From ee6f1bce6003847eb2579a416c77e826ed2d895a Mon Sep 17 00:00:00 2001 From: Aaron Janeiro Stone Date: Fri, 17 Jan 2025 12:21:13 -0500 Subject: [PATCH] fix: docgen refactor: cleanup fix: to_request_fields empty detection refactor: typing refactor: prettiness --- alpaca/common/requests.py | 4 +- alpaca/trading/enums.py | 4 +- alpaca/trading/models.py | 4 +- alpaca/trading/requests.py | 43 +++--- examples/options-trading-mleg.ipynb | 44 ++++-- tests/trading/test_trading_models.py | 131 +++++++++++++++++- .../trading_client/test_order_routes.py | 95 ++++++++----- 7 files changed, 255 insertions(+), 70 deletions(-) diff --git a/alpaca/common/requests.py b/alpaca/common/requests.py index 031e77e5..bec09274 100644 --- a/alpaca/common/requests.py +++ b/alpaca/common/requests.py @@ -76,5 +76,7 @@ def map_values(val: Any) -> Any: # {trusted_contact: {}, contact: {}, identity: None, etc} # so we do a simple list comprehension to filter out None and {} return { - key: map_values(val) for key, val in d.items() if val is not None and len(str(val)) > 0 + key: map_values(val) + for key, val in d.items() + if val is not None and val != {} and len(str(val)) > 0 } diff --git a/alpaca/trading/enums.py b/alpaca/trading/enums.py index 23203911..cd987f41 100644 --- a/alpaca/trading/enums.py +++ b/alpaca/trading/enums.py @@ -107,9 +107,7 @@ class OrderClass(str, Enum): The order classes supported by Alpaca vary based on the order's security type. The following provides a comprehensive breakdown of the supported order classes for each category: - Equity trading: simple (or ""), oco, oto, bracket. - - Options trading: - - simple (or ""). - - mleg (required for multi-leg complex option strategies) + - Options trading: Simple (or ""), mleg (required for multi-leg complex option strategies). - Crypto trading: simple (or ""). """ diff --git a/alpaca/trading/models.py b/alpaca/trading/models.py index c87bb7c7..0f360597 100644 --- a/alpaca/trading/models.py +++ b/alpaca/trading/models.py @@ -275,7 +275,9 @@ def root_validator(cls, data: dict) -> dict: if "_is_sub_mleg" not in data or not data["_is_sub_mleg"]: if "legs" not in data or data["legs"] == "": raise ValueError("legs is required for mleg orders") - if data["legs"] is not None: # it is possible when querying individual legs that this is None + if ( + data["legs"] is not None + ): # it is possible when querying individual legs that this is None if len(data["legs"]) < 1: raise ValueError("legs must have at least one order") for leg in data["legs"]: diff --git a/alpaca/trading/requests.py b/alpaca/trading/requests.py index c5e2b044..ea73e3d8 100644 --- a/alpaca/trading/requests.py +++ b/alpaca/trading/requests.py @@ -165,7 +165,8 @@ class StopLossRequest(NonEmptyRequest): stop_price: float limit_price: Optional[float] = None -class OrderLegRequest(NonEmptyRequest): + +class OptionLegRequest(NonEmptyRequest): """ Used for providing details for a leg of a multi-leg order. @@ -285,8 +286,8 @@ class OrderRequest(NonEmptyRequest): extended_hours (Optional[float]): Whether the order can be executed during regular market hours. client_order_id (Optional[str]): A string to identify which client submitted the order. order_class (Optional[OrderClass]): The class of the order. Simple orders have no other legs. - legs (Optional[List[OrderLegRequest]]): For multi-leg option orders, the legs of the order. If specified, - must contain at least 2 but no more than 4 legs. + legs (Optional[Union[List[OptionLegRequest], List[OrderRequest]]]): For multi-leg option orders, the legs of the order If specified (must contain at least 2 but no more than 4 legs for options). + Otherwise, for equities, a list of individual orders. take_profit (Optional[TakeProfitRequest]): For orders with multiple legs, an order to exit a profitable trade. stop_loss (Optional[StopLossRequest]): For orders with multiple legs, an order to exit a losing trade. position_intent (Optional[PositionIntent]): An enum to indicate the desired position strategy: BTO, BTC, STO, STC. @@ -301,7 +302,7 @@ class OrderRequest(NonEmptyRequest): order_class: Optional[OrderClass] = None extended_hours: Optional[bool] = None client_order_id: Optional[str] = None - legs: Optional[List[OrderLegRequest]] = None + legs: Optional[Union[List[OptionLegRequest], List["OrderRequest"]]] = None take_profit: Optional[TakeProfitRequest] = None stop_loss: Optional[StopLossRequest] = None position_intent: Optional[PositionIntent] = None @@ -323,18 +324,24 @@ def root_validator(cls, values: dict) -> dict: if "legs" not in values or values["legs"] is None: raise ValueError("legs is required for the mleg order class.") l_len = len(values["legs"]) - if l_len> 4: + if l_len > 4: raise ValueError("At most 4 legs are allowed for the mleg order class.") if l_len < 2: - raise ValueError("At least 2 legs are required for the mleg order class.") + raise ValueError( + "At least 2 legs are required for the mleg order class." + ) n_unique = len(set([l.symbol for l in values["legs"]])) if n_unique != l_len: raise ValueError("All legs must have unique symbols.") else: if "symbol" not in values or values["symbol"] is None: - raise ValueError("symbol is required for all order classes other than mleg.") + raise ValueError( + "symbol is required for all order classes other than mleg." + ) if "side" not in values or values["side"] is None: - raise ValueError("side is required for all order classes other than mleg.") + raise ValueError( + "side is required for all order classes other than mleg." + ) return values @@ -355,7 +362,7 @@ class MarketOrderRequest(OrderRequest): extended_hours (Optional[float]): Whether the order can be executed during regular market hours. client_order_id (Optional[str]): A string to identify which client submitted the order. order_class (Optional[OrderClass]): The class of the order. Simple orders have no other legs. - legs (Optional[List[OrderLegRequest]]): For multi-leg option orders, the legs of the order. At most 4 legs are allowed. + legs (Optional[Union[List[OptionLegRequest], List[OrderRequest]]]): For multi-leg option orders, the legs of the order. At most 4 legs are allowed for options. take_profit (Optional[TakeProfitRequest]): For orders with multiple legs, an order to exit a profitable trade. stop_loss (Optional[StopLossRequest]): For orders with multiple legs, an order to exit a losing trade. position_intent (Optional[PositionIntent]): An enum to indicate the desired position strategy: BTO, BTC, STO, STC. @@ -382,7 +389,7 @@ class StopOrderRequest(OrderRequest): extended_hours (Optional[float]): Whether the order can be executed during regular market hours. client_order_id (Optional[str]): A string to identify which client submitted the order. order_class (Optional[OrderClass]): The class of the order. Simple orders have no other legs. - legs (Optional[List[OrderLegRequest]]): For multi-leg option orders, the legs of the order. At most 4 legs are allowed. + legs (Optional[Union[List[OptionLegRequest], List[OrderRequest]]]): For multi-leg option orders, the legs of the order. At most 4 legs are allowed for options. take_profit (Optional[TakeProfitRequest]): For orders with multiple legs, an order to exit a profitable trade. stop_loss (Optional[StopLossRequest]): For orders with multiple legs, an order to exit a losing trade. stop_price (float): The price at which the stop order is converted to a market order or a stop limit @@ -413,13 +420,12 @@ class LimitOrderRequest(OrderRequest): extended_hours (Optional[float]): Whether the order can be executed during regular market hours. client_order_id (Optional[str]): A string to identify which client submitted the order. order_class (Optional[OrderClass]): The class of the order. Simple orders have no other legs. - legs (Optional[List[OrderLegRequest]]): For multi-leg option orders, the legs of the order. At most 4 legs are allowed. + legs (Optional[Union[List[OptionLegRequest], List[OrderRequest]]]): For multi-leg option orders, the legs of the order. At most 4 legs are allowed for options. take_profit (Optional[TakeProfitRequest]): For orders with multiple legs, an order to exit a profitable trade. stop_loss (Optional[StopLossRequest]): For orders with multiple legs, an order to exit a losing trade. limit_price (Optional[float]): The worst fill price for a limit or stop limit order. For the mleg order class, this - is specified such that: - - A positive value indicates a debit, representing a cost or payment to be made. - - A negative value signifies a credit, reflecting an amount to be received. + is specified such that a positive value indicates a debit (representing a cost or payment to be made) while a + negative value signifies a credit (reflecting an amount to be received). position_intent (Optional[PositionIntent]): An enum to indicate the desired position strategy: BTO, BTC, STO, STC. """ @@ -456,15 +462,14 @@ class StopLimitOrderRequest(OrderRequest): extended_hours (Optional[float]): Whether the order can be executed during regular market hours. client_order_id (Optional[str]): A string to identify which client submitted the order. order_class (Optional[OrderClass]): The class of the order. Simple orders have no other legs. - legs (Optional[List[OrderLegRequest]]): For multi-leg option orders, the legs of the order. At most 4 legs are allowed. + legs (Optional[Union[List[OptionLegRequest], List[OrderRequest]]]): For multi-leg option orders, the legs of the order. At most 4 legs are allowed for options. take_profit (Optional[TakeProfitRequest]): For orders with multiple legs, an order to exit a profitable trade. stop_loss (Optional[StopLossRequest]): For orders with multiple legs, an order to exit a losing trade. stop_price (float): The price at which the stop order is converted to a market order or a stop limit order is converted to a limit order. limit_price (float): The worst fill price for a limit or stop limit order. For the mleg order class, this - is specified such that: - - A positive value indicates a debit, representing a cost or payment to be made. - - A negative value signifies a credit, reflecting an amount to be received. + is specified such that a positive value indicates a debit (representing a cost or payment to be made) while a + negative value signifies a credit (reflecting an amount to be received). position_intent (Optional[PositionIntent]): An enum to indicate the desired position strategy: BTO, BTC, STO, STC. """ @@ -492,7 +497,7 @@ class TrailingStopOrderRequest(OrderRequest): extended_hours (Optional[float]): Whether the order can be executed during regular market hours. client_order_id (Optional[str]): A string to identify which client submitted the order. order_class (Optional[OrderClass]): The class of the order. Simple orders have no other legs. - legs (Optional[List[OrderLegRequest]]): For multi-leg option orders, the legs of the order. At most 4 legs are allowed. + legs (Optional[Union[List[OptionLegRequest], List[OrderRequest]]]): For multi-leg option orders, the legs of the order. At most 4 legs are allowed for options. take_profit (Optional[TakeProfitRequest]): For orders with multiple legs, an order to exit a profitable trade. stop_loss (Optional[StopLossRequest]): For orders with multiple legs, an order to exit a losing trade. trail_price (Optional[float]): The absolute price difference by which the trailing stop will trail. diff --git a/examples/options-trading-mleg.ipynb b/examples/options-trading-mleg.ipynb index 4d80a808..acc56be9 100644 --- a/examples/options-trading-mleg.ipynb +++ b/examples/options-trading-mleg.ipynb @@ -25,8 +25,6 @@ "metadata": {}, "cell_type": "code", "source": [ - "from alpaca.trading import LimitOrderRequest\n", - "\n", "# Please change the following to your own PAPER api key and secret\n", "# or set them as environment variables (ALPACA_API_KEY, ALPACA_SECRET_KEY).\n", "# You can get them from https://alpaca.markets/\n", @@ -41,10 +39,7 @@ "# Below are the variables for development this documents\n", "# Please do not change these variables\n", "\n", - "trade_api_url = None\n", - "trade_api_wss = None\n", - "data_api_url = None\n", - "option_stream_data_wss = None" + "trade_api_url = None" ], "id": "4ec3fa9be63ec8be", "outputs": [], @@ -95,8 +90,9 @@ "from alpaca.trading.requests import (\n", " GetOptionContractsRequest,\n", " MarketOrderRequest,\n", - " OrderLegRequest,\n", - " ReplaceOrderRequest\n", + " OptionLegRequest,\n", + " ReplaceOrderRequest,\n", + " LimitOrderRequest,\n", ")\n", "from alpaca.trading.enums import (\n", " AssetStatus,\n", @@ -238,7 +234,7 @@ " cts = trade_client.get_option_contracts(req)\n", "\n", " c = get_least_error_contract(cts.option_contracts, optimal_price)\n", - " order_legs.append(OrderLegRequest(\n", + " order_legs.append(OptionLegRequest(\n", " symbol=c.symbol,\n", " side=OrderSide.BUY,\n", " ratio_qty=1\n", @@ -247,7 +243,7 @@ "# We should see that the symbols are similar, like \"TSLA______C________\" and \"TSLA______P________\",\n", "# with all values marked as \"_\" being the same in both symbols.\n", "# Such is because we expect only the contract type (call or put, C or P) to be different.\n", - "order_legs\n" + "order_legs" ], "id": "c5d3eef0fcabc15e", "outputs": [], @@ -271,6 +267,30 @@ "outputs": [], "execution_count": null }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# Note that we can query via order ids or client order ids for the whole order or individual legs\n", + "\n", + "# Query by the order's client id\n", + "q1 = trade_client.get_order_by_client_id(res.client_order_id)\n", + "\n", + "# Query by the whole order's id\n", + "q2 = trade_client.get_order_by_id(res.id)\n", + "\n", + "# Query just the first leg's client id\n", + "q3 = trade_client.get_order_by_client_id(res.legs[0].client_order_id)\n", + "\n", + "# Query by first leg's id\n", + "q4 = trade_client.get_order_by_id(res.legs[0].id)\n", + "\n", + "print(f\"Q1: {q1}\\n===\\nQ2: {q2}\\n===\\nQ3: {q3}\\n===\\nQ4: {q4}\")" + ], + "id": "626f69b5d3a81150", + "outputs": [], + "execution_count": null + }, { "metadata": {}, "cell_type": "markdown", @@ -296,6 +316,7 @@ "C = optimal_price + (stddev / 2)\n", "A = B - stddev\n", "D = C + stddev\n", + "\n", "print(f\"A: {A}, B: {B}, C: {C}, D: {D}\")" ], "id": "88d3d516e5e20270", @@ -389,7 +410,7 @@ "\n", "order_legs_m = []\n", "for buff in buffers:\n", - " order_legs_m.append(OrderLegRequest(\n", + " order_legs_m.append(OptionLegRequest(\n", " symbol=buff.contract.symbol,\n", " side=OrderSide.BUY if buff.is_buy else OrderSide.SELL,\n", " ratio_qty=1\n", @@ -489,7 +510,6 @@ " req = ReplaceOrderRequest(\n", " qty=55,\n", " )\n", - "\n", " res = trade_client.replace_order_by_id(res.id, req)\n", " print(f\"Replaced order: {res}\")\n", "else:\n", diff --git a/tests/trading/test_trading_models.py b/tests/trading/test_trading_models.py index a5e3b962..d8002c30 100644 --- a/tests/trading/test_trading_models.py +++ b/tests/trading/test_trading_models.py @@ -1,10 +1,12 @@ -from alpaca.trading.enums import OrderSide, OrderType, TimeInForce +from alpaca.trading.enums import OrderSide, OrderType, TimeInForce, OrderClass from alpaca.trading.requests import ( MarketOrderRequest, TrailingStopOrderRequest, LimitOrderRequest, + OptionLegRequest, ) import pytest +import warnings def test_has_qty_or_notional_but_not_both(): @@ -75,3 +77,130 @@ def test_trailing_stop_order_type(): ) assert "Both trail_percent and trail_price cannot be set." in str(e.value) + + +def test_mleg_options() -> None: + symbols = [ + "AAPL250117P00200000", + "AAPL250117P00250000", + "AAPL250117P00300000", + "AAPL250117P00350000", + "AAPL250117P00400000", + ] + + def kwargs_as_string(**kwargs): + return ", ".join([f"{k}={v}" for k, v in kwargs.items()]) + + def order_request_factory(is_market: bool): + if is_market: + + def factory(warn_validated: bool = True, **kwargs): + o = MarketOrderRequest(**kwargs) + if warn_validated: + warnings.warn( + f"MarketOrderRequest({kwargs_as_string(**kwargs)}) passed validation!" + ) + return o + + else: + + def factory(warn_validated: bool = True, **kwargs): + o = LimitOrderRequest(limit_price=1, **kwargs) + if warn_validated: + warnings.warn( + f"LimitOrderRequest({kwargs_as_string(**kwargs)}) passed validation!" + ) + return o + + return factory + + for is_mkt in [True, False]: + o_req = order_request_factory(is_mkt) + + # Good requests + for sym_index in range(2, 5): + o_req( + warn_validated=False, + qty=1, + time_in_force=TimeInForce.DAY, + order_class=OrderClass.MLEG, + legs=[ + OptionLegRequest(symbol=symbol, ratio_qty=1, side=OrderSide.BUY) + for symbol in symbols[:sym_index] + ], + ) + + # Bad requests + with pytest.raises(ValueError) as e: + # Missing qty + o_req( + time_in_force=TimeInForce.DAY, + order_class=OrderClass.MLEG, + legs=[ + OptionLegRequest( + symbol=symbols[0], ratio_qty=1, side=OrderSide.BUY + ), + OptionLegRequest( + symbol=symbols[1], ratio_qty=1, side=OrderSide.SELL + ), + ], + ) + assert "At least one of qty or notional must be provided" in str(e.value) + + with pytest.raises(ValueError) as e: + # Too few legs + o_req( + qty=1, + time_in_force=TimeInForce.DAY, + order_class=OrderClass.MLEG, + legs=[ + OptionLegRequest(symbol=symbols[0], ratio_qty=1, side=OrderSide.BUY) + ], + ) + assert "At least 2 legs are required for the mleg order class" in str(e.value) + + with pytest.raises(ValueError) as e: + # Too many legs + o_req( + qty=1, + time_in_force=TimeInForce.DAY, + order_class=OrderClass.MLEG, + legs=[ + OptionLegRequest(symbol=symbol, ratio_qty=1, side=OrderSide.BUY) + for symbol in symbols + ], + ) + assert "At most 4 legs are allowed for the mleg order class." in str(e.value) + + with pytest.raises(ValueError) as e: + # Missing legs + o_req(qty=1, time_in_force=TimeInForce.DAY, order_class=OrderClass.MLEG) + assert "legs is required for the mleg order class." in str(e.value) + + with pytest.raises(ValueError) as e: + # Repeated symbols across legs + o_req( + qty=1, + time_in_force=TimeInForce.DAY, + order_class=OrderClass.MLEG, + legs=[ + OptionLegRequest( + symbol=symbols[0], ratio_qty=1, side=OrderSide.BUY + ), + OptionLegRequest( + symbol=symbols[0], ratio_qty=1, side=OrderSide.SELL + ), + ], + ) + + assert "All legs must have unique symbols." in str(e.value) + + with pytest.raises(ValueError) as e: + # Legs in non-MLEG order + o_req( + qty=1, + time_in_force=TimeInForce.DAY, + legs=[ + OptionLegRequest(symbol=symbols[0], ratio_qty=1, side=OrderSide.BUY) + ], + ) diff --git a/tests/trading/trading_client/test_order_routes.py b/tests/trading/trading_client/test_order_routes.py index 604fef7d..74223fc3 100644 --- a/tests/trading/trading_client/test_order_routes.py +++ b/tests/trading/trading_client/test_order_routes.py @@ -19,7 +19,7 @@ GetOrdersRequest, LimitOrderRequest, MarketOrderRequest, - OrderLegRequest, + OptionLegRequest, ReplaceOrderRequest, StopLossRequest, TakeProfitRequest, @@ -627,33 +627,38 @@ def test_order_position_intent(reqmock, trading_client: TradingClient): def test_mleg_request_validation() -> None: - symbols = ["AAPL250117P00200000", - "AAPL250117P00250000", - "AAPL250117P00300000", - "AAPL250117P00350000", - "AAPL250117P00400000"] + symbols = [ + "AAPL250117P00200000", + "AAPL250117P00250000", + "AAPL250117P00300000", + "AAPL250117P00350000", + "AAPL250117P00400000", + ] def kwargs_as_string(**kwargs): return ", ".join([f"{k}={v}" for k, v in kwargs.items()]) def order_request_factory(is_market: bool): if is_market: + def factory(warn_validated: bool = True, **kwargs): - o = MarketOrderRequest( - **kwargs - ) + o = MarketOrderRequest(**kwargs) if warn_validated: - warnings.warn(f"MarketOrderRequest({kwargs_as_string(**kwargs)}) passed validation!") + warnings.warn( + f"MarketOrderRequest({kwargs_as_string(**kwargs)}) passed validation!" + ) return o + else: + def factory(warn_validated: bool = True, **kwargs): - o = LimitOrderRequest( - limit_price=1, - **kwargs - ) + o = LimitOrderRequest(limit_price=1, **kwargs) if warn_validated: - warnings.warn(f"LimitOrderRequest({kwargs_as_string(**kwargs)}) passed validation!") + warnings.warn( + f"LimitOrderRequest({kwargs_as_string(**kwargs)}) passed validation!" + ) return o + return factory for is_mkt in [True, False]: @@ -661,12 +666,16 @@ def factory(warn_validated: bool = True, **kwargs): # Good requests for sym_index in range(2, 5): - o_req(warn_validated=False, - qty=1, - time_in_force=TimeInForce.DAY, - order_class=OrderClass.MLEG, - legs=[OrderLegRequest(symbol=symbol, ratio_qty=1, side=OrderSide.BUY) for symbol in - symbols[:sym_index]]) + o_req( + warn_validated=False, + qty=1, + time_in_force=TimeInForce.DAY, + order_class=OrderClass.MLEG, + legs=[ + OptionLegRequest(symbol=symbol, ratio_qty=1, side=OrderSide.BUY) + for symbol in symbols[:sym_index] + ], + ) # Bad requests with pytest.raises(ValueError): @@ -674,8 +683,15 @@ def factory(warn_validated: bool = True, **kwargs): o_req( time_in_force=TimeInForce.DAY, order_class=OrderClass.MLEG, - legs=[OrderLegRequest(symbol=symbols[0], ratio_qty=1, side=OrderSide.BUY), - OrderLegRequest(symbol=symbols[1], ratio_qty=1, side=OrderSide.SELL)]) + legs=[ + OptionLegRequest( + symbol=symbols[0], ratio_qty=1, side=OrderSide.BUY + ), + OptionLegRequest( + symbol=symbols[1], ratio_qty=1, side=OrderSide.SELL + ), + ], + ) with pytest.raises(ValueError): # Too few legs @@ -683,7 +699,10 @@ def factory(warn_validated: bool = True, **kwargs): qty=1, time_in_force=TimeInForce.DAY, order_class=OrderClass.MLEG, - legs=[OrderLegRequest(symbol=symbols[0], ratio_qty=1, side=OrderSide.BUY)]) + legs=[ + OptionLegRequest(symbol=symbols[0], ratio_qty=1, side=OrderSide.BUY) + ], + ) with pytest.raises(ValueError): # Too many legs @@ -691,15 +710,15 @@ def factory(warn_validated: bool = True, **kwargs): qty=1, time_in_force=TimeInForce.DAY, order_class=OrderClass.MLEG, - legs=[OrderLegRequest(symbol=symbol, ratio_qty=1, side=OrderSide.BUY) - for symbol in symbols]) + legs=[ + OptionLegRequest(symbol=symbol, ratio_qty=1, side=OrderSide.BUY) + for symbol in symbols + ], + ) with pytest.raises(ValueError): # Missing legs - o_req( - qty=1, - time_in_force=TimeInForce.DAY, - order_class=OrderClass.MLEG) + o_req(qty=1, time_in_force=TimeInForce.DAY, order_class=OrderClass.MLEG) with pytest.raises(ValueError): # Repeated symbols across legs @@ -707,12 +726,22 @@ def factory(warn_validated: bool = True, **kwargs): qty=1, time_in_force=TimeInForce.DAY, order_class=OrderClass.MLEG, - legs=[OrderLegRequest(symbol=symbols[0], ratio_qty=1, side=OrderSide.BUY), - OrderLegRequest(symbol=symbols[0], ratio_qty=1, side=OrderSide.SELL)]) + legs=[ + OptionLegRequest( + symbol=symbols[0], ratio_qty=1, side=OrderSide.BUY + ), + OptionLegRequest( + symbol=symbols[0], ratio_qty=1, side=OrderSide.SELL + ), + ], + ) with pytest.raises(ValueError): # Legs in non-MLEG order o_req( qty=1, time_in_force=TimeInForce.DAY, - legs=[OrderLegRequest(symbol=symbols[0], ratio_qty=1, side=OrderSide.BUY)]) + legs=[ + OptionLegRequest(symbol=symbols[0], ratio_qty=1, side=OrderSide.BUY) + ], + )