Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IB Fixes for order objects #638

Merged
merged 5 commits into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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