Skip to content

Commit

Permalink
Merge pull request #638 from Lumiwealth/ib-retry-policy
Browse files Browse the repository at this point in the history
IB Fixes for order objects
  • Loading branch information
grzesir authored Nov 28, 2024
2 parents 9539132 + e8ec025 commit 4067047
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 41 deletions.
39 changes: 31 additions & 8 deletions lumibot/brokers/interactive_brokers_rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,11 +192,13 @@ def _parse_broker_order(self, response, strategy_name, strategy_object=None):
order.quantity = totalQuantity
order.asset = Asset(symbol=response['ticker'], asset_type="multileg")
order.side = response['side']
order.identifier = response['orderId']

order.child_orders = []

# Parse the legs of the combo order.
legs = self.decode_conidex(response["conidex"])
n=0
for leg, ratio in legs.items():
# Create the object with just the conId
# TODO check if all legs using the same response is an issue; test with covered calls
Expand All @@ -205,7 +207,10 @@ def _parse_broker_order(self, response, strategy_name, strategy_object=None):
response=response,
quantity=float(ratio) * totalQuantity,
conId=leg,
parent_identifier=order.identifier,
child_order_number=str(n)
)
n+=1
order.child_orders.append(child_order)

else:
Expand All @@ -224,7 +229,7 @@ def _parse_broker_order(self, response, strategy_name, strategy_object=None):
order.update_raw(response)
return order

def _parse_order_object(self, strategy_name, response, quantity, conId):
def _parse_order_object(self, strategy_name, response, quantity, conId, parent_identifier=None, child_order_number=None):
if quantity < 0:
side = "SELL"
quantity = -quantity
Expand Down Expand Up @@ -286,6 +291,7 @@ def _parse_order_object(self, strategy_name, response, quantity, conId):
asset,
quantity=Decimal(quantity),
side=side.lower(),
status=response['status'],
limit_price=limit_price,
stop_price=stop_price,
time_in_force=time_in_force,
Expand All @@ -294,6 +300,12 @@ def _parse_order_object(self, strategy_name, response, quantity, conId):
avg_fill_price=response["avgPrice"] if "avgPrice" in response else None
)

if parent_identifier is not None:
order.parent_identifier=parent_identifier

if child_order_number:
order.identifier = f'{parent_identifier}-{child_order_number}'

return order

def _pull_broker_all_orders(self):
Expand Down Expand Up @@ -643,16 +655,19 @@ def _submit_order(self, order: Order) -> Order:
try:
order_data = self.get_order_data_from_orders([order])
response = self.data_source.execute_order(order_data)

if response is None:
self._log_order_status(order, "failed", success=False)
msg = "Broker returned no response"
self.stream.dispatch(self.ERROR_ORDER, order=order, error_msg=msg)
return order
else:
self._log_order_status(order, "executed", success=True)

self._log_order_status(order, "executed", success=True)

order.identifier = response[0]["order_id"]
self._unprocessed_orders.append(order)
order.status=Order.OrderStatus.SUBMITTED

self.stream.dispatch(self.NEW_ORDER, order=order)

return order
Expand Down Expand Up @@ -705,8 +720,15 @@ def submit_orders(

order = Order(orders[0].strategy)
order.order_class = Order.OrderClass.MULTILEG
order.child_orders = orders
order.identifier = response[0]["order_id"]
order.status=Order.OrderStatus.SUBMITTED
order.side = order_data['orders'][0]['side'].lower() if order_data is not None else None

order.child_orders = orders
for n, child_order in enumerate(order.child_orders):
child_order.identifier = f'{order.identifier}-{n}'
child_order.parent_identifier = order.identifier
order.status=Order.OrderStatus.SUBMITTED

self._unprocessed_orders.append(order)
self.stream.dispatch(self.NEW_ORDER, order=order)
Expand All @@ -731,6 +753,8 @@ def submit_orders(
self._unprocessed_orders.append(order)
self.stream.dispatch(self.NEW_ORDER, order=order)
self._log_order_status(order, "executed", success=True)
order.status=Order.OrderStatus.SUBMITTED

order_id += 1

return orders
Expand Down Expand Up @@ -931,9 +955,6 @@ def get_order_data_multileg(
conidex += ","
conidex += f"{conid}/{quantity // order_quantity}"

# Set the side to "BUY" for the multileg order
side = "BUY"

if not orders:
logging.error("Orders list cannot be empty")

Expand Down Expand Up @@ -1122,7 +1143,9 @@ def do_polling(self):
stored_order.quantity = order.quantity
stored_children = [stored_orders[o.identifier] if o.identifier in stored_orders else o
for o in order.child_orders]
stored_order.child_orders = stored_children

if stored_children:
stored_order.child_orders = stored_children

# Handle status changes
if not order.equivalent_status(stored_order):
Expand Down
53 changes: 28 additions & 25 deletions lumibot/data_sources/interactive_brokers_rest_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ def suppress_warnings(self):
url = f"{self.base_url}/iserver/questions/suppress"
json = {"messageIds": ["o451", "o383", "o354", "o163"]}

self.post_to_endpoint(url, json=json, allow_fail=False)
self.post_to_endpoint(url, json=json, description="Suppressing server warnings", allow_fail=False)

def fetch_account_id(self):
if self.account_id is not None:
Expand Down Expand Up @@ -246,7 +246,7 @@ def get_account_balances(self):

return response

def handle_http_errors(self, response):
def handle_http_errors(self, response, description):
to_return = None
re_msg = None
is_error = False
Expand Down Expand Up @@ -279,7 +279,7 @@ def handle_http_errors(self, response):
confirm_response = self.post_to_endpoint(
confirm_url,
{"confirmed": True},
"Confirming order",
description="Confirming Order",
silent=True,
allow_fail=True
)
Expand All @@ -288,25 +288,25 @@ def handle_http_errors(self, response):
status_code = 200
response_json = orders

if "no bridge" in error_message.lower() or "not authenticated" in error_message.lower():
if 'Please query /accounts first' in error_message:
self.ping_iserver()
retrying = True
re_msg = "Not Authenticated"
re_msg = f"Task {description} failed: Lumibot got Deauthenticated"

elif "no bridge" in error_message.lower() or "not authenticated" in error_message.lower():
retrying = True
re_msg = f"Task {description} failed: Not Authenticated"

elif 200 <= status_code < 300:
to_return = response_json
retrying = False

elif status_code == 429:
retrying = True
re_msg = "You got rate limited"
re_msg = f"Task {description} failed: You got rate limited"

elif status_code == 503:
if any("Please query /accounts first" in str(value) for value in response_json.values()):
self.ping_iserver()
re_msg = "Lumibot got Deauthenticated"
else:
re_msg = "Internal server error, should fix itself soon"

re_msg = f"Task {description} failed: Internal server error, should fix itself soon"
retrying = True

elif status_code == 500:
Expand All @@ -316,7 +316,7 @@ def handle_http_errors(self, response):

elif status_code == 410:
retrying = True
re_msg = "The bridge blew up"
re_msg = f"Task {description} failed: The bridge blew up"

elif 400 <= status_code < 500:
to_return = response_json
Expand All @@ -336,15 +336,15 @@ def get_from_endpoint(self, url, description="", silent=False, allow_fail=True):
try:
while retrying or not allow_fail:
response = requests.get(url, verify=False)
retrying, re_msg, is_error, to_return = self.handle_http_errors(response)
retrying, re_msg, is_error, to_return = self.handle_http_errors(response, description)

if re_msg is not None:
if not silent and retries == 0:
logging.warning(f'{re_msg}. Retrying...')
logging.warning(colored(f'{re_msg}. Retrying...', "yellow"))

elif is_error:
if not silent and retries == 0:
logging.error(f"Task {description} failed: {to_return}")
logging.error(colored(f"Task {description} failed: {to_return}", "red"))

else:
allow_fail = True
Expand All @@ -367,15 +367,15 @@ def post_to_endpoint(self, url, json: dict, description="", silent=False, allow_
try:
while retrying or not allow_fail:
response = requests.post(url, json=json, verify=False)
retrying, re_msg, is_error, to_return = self.handle_http_errors(response)
retrying, re_msg, is_error, to_return = self.handle_http_errors(response, description)

if re_msg is not None:
if not silent and retries == 0:
logging.warning(f'{re_msg}. Retrying...')
logging.warning(colored(f'{re_msg}. Retrying...', "yellow"))

elif is_error:
if not silent and retries == 0:
logging.error(f"Task {description} failed: {to_return}")
logging.error(colored(f"Task {description} failed: {to_return}", "red"))

else:
allow_fail = True
Expand All @@ -398,15 +398,15 @@ def delete_to_endpoint(self, url, description="", silent=False, allow_fail=True)
try:
while retrying or not allow_fail:
response = requests.delete(url, verify=False)
retrying, re_msg, is_error, to_return = self.handle_http_errors(response)
retrying, re_msg, is_error, to_return = self.handle_http_errors(response, description)

if re_msg is not None:
if not silent and retries == 0:
logging.warning(f'{re_msg}. Retrying...')
logging.warning(colored(f'{re_msg}. Retrying...', "yellow"))

elif is_error:
if not silent and retries == 0:
logging.error(f"Task {description} failed: {to_return}")
logging.error(colored(f"Task {description} failed: {to_return}", "red"))

else:
allow_fail = True
Expand Down Expand Up @@ -463,7 +463,10 @@ def get_broker_all_orders(self):
url, "Getting open orders", allow_fail=False
)

return [order for order in response['orders'] if order.get('totalSize', 0) != 0]
if 'orders' in response and isinstance(response['orders'], list):
return [order for order in response['orders'] if order.get('totalSize', 0) != 0]

return []

def get_order_info(self, orderid):
self.ping_iserver()
Expand All @@ -480,7 +483,7 @@ def execute_order(self, order_data):
self.ping_iserver()

url = f"{self.base_url}/iserver/account/{self.account_id}/orders"
response = self.post_to_endpoint(url, order_data)
response = self.post_to_endpoint(url, order_data, description="Executing order")

if isinstance(response, list) and "order_id" in response[0]:
# success
Expand All @@ -505,7 +508,7 @@ def delete_order(self, order):
self.ping_iserver()
orderId = order.identifier
url = f"{self.base_url}/iserver/account/{self.account_id}/order/{orderId}"
status = self.delete_to_endpoint(url)
status = self.delete_to_endpoint(url, description=f"Deleting order {orderId}")
if status:
logging.info(
colored(f"Order with ID {orderId} canceled successfully.", "green")
Expand Down
20 changes: 12 additions & 8 deletions lumibot/entities/order.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,16 +364,20 @@ def __init__(
)

def is_buy_order(self):
return self.side == self.OrderSide.BUY or \
self.side == self.OrderSide.BUY_TO_OPEN or \
self.side == self.OrderSide.BUY_TO_COVER or \
self.side == self.OrderSide.BUY_TO_CLOSE
return self.side is not None and (
self.side.lower() == self.OrderSide.BUY or
self.side.lower() == self.OrderSide.BUY_TO_OPEN or
self.side.lower() == self.OrderSide.BUY_TO_COVER or
self.side.lower() == self.OrderSide.BUY_TO_CLOSE
)

def is_sell_order(self):
return self.side == self.OrderSide.SELL or \
self.side == self.OrderSide.SELL_SHORT or \
self.side == self.OrderSide.SELL_TO_OPEN or \
self.side == self.OrderSide.SELL_TO_CLOSE
return self.side is not None and (
self.side.lower() == self.OrderSide.SELL or
self.side.lower() == self.OrderSide.SELL_SHORT or
self.side.lower() == self.OrderSide.SELL_TO_OPEN or
self.side.lower() == self.OrderSide.SELL_TO_CLOSE
)

def is_parent(self) -> bool:
"""
Expand Down

0 comments on commit 4067047

Please sign in to comment.