-
Notifications
You must be signed in to change notification settings - Fork 214
Alpaca multi leg options and tweaks #702
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
base: dev
Are you sure you want to change the base?
Changes from 2 commits
687fc02
cef4c0f
9627f4a
6be86f3
b358ae2
cac5d00
46ba483
3b7645e
53a1a72
4c93bcb
0bc7be0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,7 @@ | |
import pandas_market_calendars as mcal | ||
from alpaca.trading.client import TradingClient | ||
from alpaca.trading.stream import TradingStream | ||
from alpaca.trading.requests import GetOrdersRequest | ||
from dateutil import tz | ||
from termcolor import colored | ||
|
||
|
@@ -266,10 +267,25 @@ def _parse_broker_position(self, broker_position, strategy, orders=None): | |
symbol=position.symbol.replace("USD", ""), | ||
asset_type="crypto", | ||
) | ||
elif position.asset_class == "option": | ||
elif position.asset_class == "us_option": | ||
underlying = ''.join([char for char in position.symbol[:-9] if not char.isdigit()]) | ||
|
||
year = int('20' + position.symbol[len(position.symbol)-15:len(position.symbol)-13]) | ||
month = int(position.symbol[len(position.symbol)-13:len(position.symbol)-11]) | ||
day = int(position.symbol[len(position.symbol)-11:len(position.symbol)-9]) | ||
exp = datetime.datetime(year, month, day).date() | ||
|
||
right = 'CALL' if position.symbol[-9] == 'C' else 'PUT' | ||
|
||
strike_price = position.symbol[-8:-3] | ||
strike_price = float(strike_price) | ||
|
||
asset = Asset( | ||
symbol=position.symbol, | ||
symbol=underlying, | ||
asset_type="option", | ||
expiration=exp, | ||
strike=strike_price, | ||
right=right | ||
) | ||
else: | ||
asset = Asset( | ||
|
@@ -346,20 +362,57 @@ def _parse_broker_order(self, response, strategy_name, strategy_object=None): | |
else: | ||
symbol = response.symbol | ||
|
||
order = Order( | ||
strategy_name, | ||
Asset( | ||
symbol=symbol, | ||
asset_type=self.map_asset_type(response.asset_class), | ||
), | ||
Decimal(response.qty), | ||
response.side, | ||
limit_price=response.limit_price, | ||
stop_price=response.stop_price, | ||
time_in_force=response.time_in_force, | ||
# TODO: remove hardcoding in case Alpaca allows crypto to crypto trading | ||
quote=Asset(symbol="USD", asset_type="forex"), | ||
) | ||
asset_type = self.map_asset_type(response.asset_class) | ||
|
||
qty = Decimal(response.qty) if response.qty != None else Decimal(response.filled_qty) | ||
fill_price = Decimal(response.filled_avg_price) if response.filled_avg_price != None else 0 | ||
|
||
if asset_type == Asset.AssetType.OPTION: | ||
underlying = ''.join([char for char in symbol[:-9] if not char.isdigit()]) | ||
|
||
year = int('20' + symbol[len(symbol)-15:len(symbol)-13]) | ||
month = int(symbol[len(symbol)-13:len(symbol)-11]) | ||
day = int(symbol[len(symbol)-11:len(symbol)-9]) | ||
exp = datetime.datetime(year, month, day).date() | ||
|
||
right = 'CALL' if symbol[-9] == 'C' else 'PUT' | ||
|
||
strike_price = symbol[-8:-3] | ||
strike_price = float(strike_price) | ||
GrizzlyEnglish marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
asset = Asset( | ||
symbol=underlying, | ||
asset_type=Asset.AssetType.OPTION, | ||
expiration=exp, | ||
strike=strike_price, | ||
right=right | ||
) | ||
order = Order( | ||
strategy_name, | ||
asset, | ||
qty, | ||
response.side, | ||
limit_price=response.limit_price, | ||
stop_price=response.stop_price, | ||
time_in_force=response.time_in_force, | ||
avg_fill_price=fill_price | ||
) | ||
else: | ||
order = Order( | ||
strategy_name, | ||
Asset( | ||
symbol=symbol, | ||
asset_type=asset_type, | ||
), | ||
qty, | ||
response.side, | ||
limit_price=response.limit_price, | ||
stop_price=response.stop_price, | ||
time_in_force=response.time_in_force, | ||
avg_fill_price=fill_price, | ||
# TODO: remove hardcoding in case Alpaca allows crypto to crypto trading | ||
quote=Asset(symbol="USD", asset_type="forex"), | ||
) | ||
order.set_identifier(response.id) | ||
order.status = response.status | ||
order.update_raw(response) | ||
|
@@ -372,7 +425,8 @@ def _pull_broker_order(self, identifier): | |
|
||
def _pull_broker_all_orders(self): | ||
"""Get the broker orders""" | ||
return self.api.get_orders() | ||
orders = self.api.get_orders(GetOrdersRequest(status='all')) | ||
return orders | ||
|
||
def _flatten_order(self, order): | ||
"""Some submitted orders may trigger other orders. | ||
|
@@ -387,6 +441,126 @@ def _flatten_order(self, order): | |
|
||
return orders | ||
|
||
def _submit_multileg_order(self, orders, order_type="market", duration="day", price=None, tag=None) -> Order: | ||
""" | ||
Submit a multi-leg order to Tradier. This function will submit the multi-leg order to Tradier. | ||
|
||
Parameters | ||
---------- | ||
orders: list[Order] | ||
List of orders to submit | ||
order_type: str | ||
The type of multi-leg order to submit. Valid values are ('market', 'debit', 'credit', 'even'). Default is 'market'. | ||
duration: str | ||
The duration of the order. Valid values are ('day', 'gtc', 'pre', 'post'). Default is 'day'. | ||
price: float | ||
The limit price for the order. Required for 'debit' and 'credit' order types. | ||
tag: str | ||
The tag to associate with the order. | ||
|
||
Returns | ||
------- | ||
parent order of the multi-leg orders | ||
""" | ||
|
||
# Check if the order type is valid | ||
if order_type not in ["market", "debit", "credit", "even"]: | ||
raise ValueError(f"Invalid order type '{order_type}' for multi-leg order.") | ||
|
||
# Check if the duration is valid | ||
if duration not in ["day", "gtc", "pre", "post"]: | ||
raise ValueError(f"Invalid duration {duration} for multi-leg order.") | ||
|
||
# Check if the price is required | ||
if order_type in ["debit", "credit"] and price is None: | ||
raise ValueError(f"Price is required for '{order_type}' order type.") | ||
|
||
# Check that all the order objects have the same symbol | ||
if len(set([order.asset.symbol for order in orders])) > 1: | ||
raise ValueError("All orders in a multi-leg order must have the same symbol.") | ||
|
||
# Get the symbol from the first order | ||
symbol = orders[0].asset.symbol | ||
|
||
# Create the legs for the multi-leg order | ||
legs = [] | ||
for order in orders: | ||
leg = { | ||
"symbol": order.asset.symbol, | ||
"ratio_qty": order.quantity, | ||
"side": order.side, | ||
"position_intent": order.position_intent | ||
} | ||
legs.append(leg) | ||
|
||
parent_asset = Asset(symbol=symbol) | ||
parent_order = Order( | ||
asset=parent_asset, | ||
strategy=orders[0].strategy, | ||
order_class=Order.OrderClass.MULTILEG, | ||
side=orders[0].side, | ||
quantity=orders[0].quantity, | ||
type=orders[0].type, | ||
time_in_force=duration, | ||
limit_price=price if price != None else orders[0].limit_price, | ||
tag=tag, | ||
status=Order.OrderStatus.SUBMITTED, | ||
legs=legs | ||
GrizzlyEnglish marked this conversation as resolved.
Show resolved
Hide resolved
|
||
) | ||
order_response = self.submit_order(parent_order) | ||
for o in orders: | ||
o.parent_identifier = order_response.identifier | ||
|
||
parent_order.child_orders = orders | ||
parent_order.update_raw(order_response) # This marks order as 'transmitted' | ||
self._unprocessed_orders.append(parent_order) | ||
self.stream.dispatch(self.NEW_ORDER, order=parent_order) | ||
return parent_order | ||
|
||
def _submit_orders(self, orders, is_multileg=False, order_type=None, duration="day", price=None): | ||
""" | ||
Submit multiple orders to the broker. This function will submit the orders in the order they are provided. | ||
If any order fails to submit, the function will stop submitting orders and return the last successful order. | ||
|
||
Parameters | ||
---------- | ||
orders: list[Order] | ||
List of orders to submit | ||
is_multileg: bool | ||
Whether the order is a multi-leg order. Default is False. | ||
order_type: str | ||
The type of multi-leg order to submit, if applicable. Valid values are ('market', 'debit', 'credit', 'even'). Default is 'market'. | ||
duration: str | ||
The duration of the order. Valid values are ('day', 'gtc', 'pre', 'post'). Default is 'day'. | ||
price: float | ||
The limit price for the order. Required for 'debit' and 'credit' order types. | ||
|
||
Returns | ||
------- | ||
Order | ||
The list of processed order objects. | ||
""" | ||
|
||
# Check if order_type is set, if not, set it to 'market' | ||
if order_type is None: | ||
order_type = "market" | ||
|
||
# Check if the orders are empty | ||
if not orders or len(orders) == 0: | ||
return | ||
|
||
# Check if it is a multi-leg order | ||
if is_multileg: | ||
parent_order = self._submit_multileg_order(orders, order_type, duration, price) | ||
return [parent_order] | ||
|
||
else: | ||
# Submit each order | ||
for order in orders: | ||
self._submit_order(order) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. _submit_order() returns an order object with the Identifier filled in. This is "usually" the same as the order object passed in, but it is an important thing to track so that the proper items get passed to lifecycle methods like "on_new_order()" or "on_filled_order()" later. Please collect all of the orders passed back by _submit_order() and return them as well. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I got this as well, I take the newly submitted orders and return those, rather than the passed in orders |
||
|
||
return orders | ||
|
||
def _submit_order(self, order): | ||
"""Submit an order for an asset""" | ||
|
||
|
@@ -425,6 +599,7 @@ def _submit_order(self, order): | |
"stop_price": str(order.stop_price) if order.stop_price else None, | ||
"trail_price": str(order.trail_price) if order.trail_price else None, | ||
"trail_percent": order.trail_percent, | ||
"legs": order.legs | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The only place that I can see where "legs" was ever set was in the other submit function "_submit_multileg_order()". It seems like this will never be valid at this point in time and that child_orders should be referenced instead to build out the required legs. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I commented on the other, but the same thing applies here. As I understand alpacapy wants the legs in the order submission, not as an array of orders. I could be wrong though There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All of that is fine, you do have to Send it to Alpaca in the legs format it expects. However, you don't need to Store it in the order as "order.legs". This is because the structure of "legs" is different for each broker and it can be confusing months from now if there are updates that need to be made. I just think that "legs" should just live inside the alpaca.py file and not be a generic item in Order. Whenever a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I am with you almost got it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
} | ||
# Remove items with None values | ||
kwargs = {k: v for k, v in kwargs.items() if v} | ||
|
Uh oh!
There was an error while loading. Please reload this page.