diff --git a/README.md b/README.md index 54cb90ed..fd201d95 100644 --- a/README.md +++ b/README.md @@ -271,3 +271,4 @@ We have put together some examples in jupyter notebooks so that you can start de * [Stocks](https://github.com/alpacahq/alpaca-py/blob/master/examples/stocks-trading-basic.ipynb) * [Options](https://github.com/alpacahq/alpaca-py/blob/master/examples/options-trading-basic.ipynb) * [Crypto](https://github.com/alpacahq/alpaca-py/blob/master/examples/crypto-trading-basic.ipynb) +* [Multi-Leg Options](https://github.com/alpacahq/alpaca-py/blob/master/examples/options-trading-mleg.ipynb) diff --git a/alpaca/common/requests.py b/alpaca/common/requests.py index ef77d5cb..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 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 4e09b437..fe19314c 100644 --- a/alpaca/trading/enums.py +++ b/alpaca/trading/enums.py @@ -107,11 +107,12 @@ 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 ""). + - Options trading: simple (or ""), mleg (required for multi-leg complex options strategies). - Crypto trading: simple (or ""). """ SIMPLE = "simple" + MLEG = "mleg" BRACKET = "bracket" OCO = "oco" OTO = "oto" diff --git a/alpaca/trading/models.py b/alpaca/trading/models.py index 630bced8..3b011ef3 100644 --- a/alpaca/trading/models.py +++ b/alpaca/trading/models.py @@ -26,7 +26,7 @@ TradeConfirmationEmail, TradeEvent, ) -from pydantic import Field +from pydantic import Field, model_validator class Asset(ModelWithID): @@ -182,9 +182,9 @@ class Order(ModelWithID): replaced_at (Optional[datetime]): Timestamp when the order was replaced by a new order. replaced_by (Optional[UUID]): ID of order that replaces this order. replaces (Optional[UUID]): ID of order which this order replaces. - asset_id (UUID): ID of the asset. - symbol (str): Symbol of the asset. - asset_class (AssetClass): Asset class of the asset. + asset_id (Optional[UUID]): ID of the asset. Omitted from top-level of response if the order is of mleg class. + symbol (Optional[str]): Symbol of the asset. Omitted from top-level of response if the order is of mleg class. + asset_class (Optional[AssetClass]): Asset class of the asset. Omitted from top-level of response if the order is of mleg class. notional (Optional[str]): Ordered notional amount. If entered, qty will be null. Can take up to 9 decimal points. qty (Optional[str]): Ordered quantity. If entered, notional will be null. Can take up to 9 decimal points. @@ -192,9 +192,9 @@ class Order(ModelWithID): filled_avg_price (Optional[str]): Filled average price. Can be 0 until order is processed in case order is passed outside of market hours. order_class (OrderClass): Valid values: simple, bracket, oco or oto. - order_type (OrderType): Deprecated with just type field below. - type (OrderType): Valid values: market, limit, stop, stop_limit, trailing_stop. - side (OrderSide): Valid values: buy and sell. + order_type (Optional[OrderType]): Deprecated with just type field below. Omitted from legs of mleg orders. + type (Optional[OrderType]): Valid values: market, limit, stop, stop_limit, trailing_stop. Omitted from legs of mleg orders. + side (Optional[OrderSide]): Valid values: buy and sell. Omitted from top-level of response if the order is of mleg class. time_in_force (TimeInForce): Length of time the order is in force. limit_price (Optional[str]): Limit price of the order. stop_price (Optional[str]): Stop price of the order. @@ -206,6 +206,7 @@ class Order(ModelWithID): trail_price (Optional[str]): The dollar value away from the high water mark for trailing stop orders. hwm (Optional[str]): The highest (lowest) market price seen since the trailing stop order was submitted. position_intent (Optional[PositionIntent]): Represents the desired position strategy. + ratio_qty (Optional[str]): The proportional quantity of this leg in relation to the overall multi-leg order quantity. """ client_order_id: str @@ -219,17 +220,17 @@ class Order(ModelWithID): replaced_at: Optional[datetime] = None replaced_by: Optional[UUID] = None replaces: Optional[UUID] = None - asset_id: UUID - symbol: str - asset_class: AssetClass + asset_id: Optional[UUID] = None + symbol: Optional[str] = None + asset_class: Optional[AssetClass] = None notional: Optional[str] = None qty: Optional[Union[str, float]] = None filled_qty: Optional[Union[str, float]] = None filled_avg_price: Optional[Union[str, float]] = None order_class: OrderClass - order_type: OrderType - type: OrderType - side: OrderSide + order_type: Optional[OrderType] = None + type: Optional[OrderType] = None + side: Optional[OrderSide] = None time_in_force: TimeInForce limit_price: Optional[Union[str, float]] = None stop_price: Optional[Union[str, float]] = None @@ -240,11 +241,18 @@ class Order(ModelWithID): trail_price: Optional[str] = None hwm: Optional[str] = None position_intent: Optional[PositionIntent] = None + ratio_qty: Optional[Union[str, float]] = None def __init__(self, **data: Any) -> None: if "order_class" not in data or data["order_class"] == "": data["order_class"] = OrderClass.SIMPLE + # mleg responses will give ''s that will need to be converted to None + # to avoid validation errors from pydantic + for k in ["asset_id", "symbol", "asset_class", "side", "type", "order_type"]: + if k in data and data[k] == "": + data[k] = None + super().__init__(**data) @@ -500,9 +508,9 @@ class TradeAccount(ModelWithID): (inclusive of today) options_buying_power (Optional[str]): Your buying power for options trading options_approved_level (Optional[int]): The options trading level that was approved for this account. - 0=disabled, 1=Covered Call/Cash-Secured Put, 2=Long Call/Put. + 0=disabled, 1=Covered Call/Cash-Secured Put, 2=Long Call/Put, 3=Spreads/Straddles. options_trading_level (Optional[int]): The effective options trading level of the account. This is the minimum between account options_approved_level and account configurations max_options_trading_level. - 0=disabled, 1=Covered Call/Cash-Secured Put, 2=Long + 0=disabled, 1=Covered Call/Cash-Secured Put, 2=Long, 3=Spreads/Straddles. """ account_number: str @@ -553,7 +561,7 @@ class AccountConfiguration(BaseModel): suspend_trade (bool): If true Account becomes unable to submit new orders trade_confirm_email (TradeConfirmationEmail): Controls whether Trade confirmation emails are sent. ptp_no_exception_entry (bool): If set to true then Alpaca will accept orders for PTP symbols with no exception. Default is false. - max_options_trading_level (Optional[int]): The desired maximum options trading level. 0=disabled, 1=Covered Call/Cash-Secured Put, 2=Long Call/Put. + max_options_trading_level (Optional[int]): The desired maximum options trading level. 0=disabled, 1=Covered Call/Cash-Secured Put, 2=Long Call/Put, 3=Spreads/Straddles. """ dtbp_check: DTBPCheck diff --git a/alpaca/trading/requests.py b/alpaca/trading/requests.py index 75df61de..20483286 100644 --- a/alpaca/trading/requests.py +++ b/alpaca/trading/requests.py @@ -166,6 +166,35 @@ class StopLossRequest(NonEmptyRequest): limit_price: Optional[float] = None +class OptionLegRequest(NonEmptyRequest): + """ + Used for providing details for a leg of a multi-leg order. + + Attributes: + symbol (str): The symbol identifier for the asset being traded. + ratio_qty (float): The proportional quantity of this leg in relation to the overall multi-leg order quantity. + side (Optional[OrderSide]): Represents the side this order was on. + position_intent (Optional[PositionIntent]): Represents the position strategy for this leg. + """ + + symbol: str + ratio_qty: float + side: Optional[OrderSide] = None + position_intent: Optional[PositionIntent] = None + + @model_validator(mode="before") + def root_validator(cls, values: dict) -> dict: + side = values.get("side", None) + position_intent = values.get("position_intent", None) + + if side is None and position_intent is None: + raise ValueError( + "at least one of side or position_intent must be provided for OptionLegRequest" + ) + + return values + + class GetOrdersRequest(NonEmptyRequest): """Contains data for submitting a request to retrieve orders. @@ -257,30 +286,35 @@ class OrderRequest(NonEmptyRequest): this class when submitting an order. Instead, use one of the order type specific classes. Attributes: - symbol (str): The symbol identifier for the asset being traded + symbol (str): The symbol identifier for the asset being traded. Required for all order classes other than + mleg. qty (Optional[float]): The number of shares to trade. Fractional qty for stocks only with market orders. + Required for mleg order class. notional (Optional[float]): The base currency value of the shares to trade. For stocks, only works with MarketOrders. **Does not work with qty**. - side (OrderSide): Whether the order will buy or sell the asset. + side (Optional[OrderSide]): Whether the order will buy or sell the asset. Either side or position_intent is required for all order classes other than mleg. type (OrderType): The execution logic type of the order (market, limit, etc). time_in_force (TimeInForce): The expiration logic of the order. 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[OptionLegRequest]]): 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. """ - symbol: str + symbol: Optional[str] = None qty: Optional[float] = None notional: Optional[float] = None - side: OrderSide + side: Optional[OrderSide] = None type: OrderType time_in_force: TimeInForce order_class: Optional[OrderClass] = None extended_hours: Optional[bool] = None client_order_id: Optional[str] = None + legs: Optional[List[OptionLegRequest]] = None take_profit: Optional[TakeProfitRequest] = None stop_loss: Optional[StopLossRequest] = None position_intent: Optional[PositionIntent] = None @@ -295,6 +329,32 @@ def root_validator(cls, values: dict) -> dict: elif qty_set and notional_set: raise ValueError("Both qty and notional can not be set.") + # mleg-related checks + if "order_class" in values and values["order_class"] == OrderClass.MLEG: + if not qty_set: + raise ValueError("qty is required for the mleg order class.") + 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: + 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." + ) + 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." + ) + if "side" not in values or values["side"] is None: + raise ValueError( + "side is required for all order classes other than mleg." + ) + return values @@ -303,16 +363,18 @@ class MarketOrderRequest(OrderRequest): Used to submit a market order. Attributes: - symbol (str): The symbol identifier for the asset being traded + symbol (str): The symbol identifier for the asset being traded. Required for all order classes other than + mleg. qty (Optional[float]): The number of shares to trade. Fractional qty for stocks only with market orders. notional (Optional[float]): The base currency value of the shares to trade. For stocks, only works with MarketOrders. **Does not work with qty**. - side (OrderSide): Whether the order will buy or sell the asset. + side (OrderSide): Whether the order will buy or sell the asset. Required for all order classes other than mleg. type (OrderType): The execution logic type of the order (market, limit, etc). time_in_force (TimeInForce): The expiration logic of the order. 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[OptionLegRequest]]): 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. @@ -339,6 +401,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[OptionLegRequest]]): 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 @@ -369,9 +432,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[OptionLegRequest]]): 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. + 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) 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. """ @@ -384,6 +450,8 @@ def __init__(self, **data: Any) -> None: @model_validator(mode="before") def root_validator(cls, values: dict) -> dict: + # noinspection PyCallingNonCallable + super().root_validator(values) if values.get("order_class", "") != OrderClass.OCO: limit_price = values.get("limit_price", None) if limit_price is None: @@ -406,11 +474,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[OptionLegRequest]]): 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. + 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) 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. """ @@ -438,6 +509,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[OptionLegRequest]]): 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. @@ -455,6 +527,8 @@ def __init__(self, **data: Any) -> None: @model_validator(mode="before") def root_validator(cls, values: dict) -> dict: + # noinspection PyCallingNonCallable + super().root_validator(values) trail_percent_set = ( "trail_percent" in values and values["trail_percent"] is not None ) @@ -522,7 +596,8 @@ class GetOptionContractsRequest(NonEmptyRequest): strike_price_gte (Optional[str]): The option contract strike price greater than or equal to. strike_price_lte (Optional[str]): The option contract strike price less than or equal to. limit (Optional[int]): The number of contracts to limit per page (default=100, max=10000). - page_token (Optional[str]): Pagination token to continue from. The value to pass here is returned in specific requests when more data is available than the request limit allows. + page_token (Optional[str]): Pagination token to continue from. The value to pass here is returned in specific + requests when more data is available than the request limit allows. """ underlying_symbols: Optional[List[str]] = None diff --git a/examples/README.md b/examples/README.md index b34b6910..7950732f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,9 +1,11 @@ ## Google Colab Notebooks -Explore the examples for Stocks, Options, and Crypto utilizing alpaca-py in the notebooks provided below. Open them in Google Colab to jumpstart your development journey! +Explore the examples for Stocks, Options, and Crypto utilizing alpaca-py in the notebooks provided below. Open them in +Google Colab to jumpstart your development journey! -| Notebooks | Open in Google Colab | -| :--------------------------------------------------------------------------------------- | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | -| [Stocks](stocks-trading-basic.ipynb) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/alpacahq/alpaca-py/blob/master/examples/stocks-trading-basic.ipynb) | -| [Options](options-trading-basic.ipynb) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/alpacahq/alpaca-py/blob/master/examples/options-trading-basic.ipynb) | -| [Crypto](crypto-trading-basic.ipynb) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/alpacahq/alpaca-py/blob/master/examples/crypto-trading-basic.ipynb) | +| Notebooks | Open in Google Colab | +|:------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:| +| [Stocks](stocks-trading-basic.ipynb) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/alpacahq/alpaca-py/blob/master/examples/stocks-trading-basic.ipynb) | +| [Options](options-trading-basic.ipynb) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/alpacahq/alpaca-py/blob/master/examples/options-trading-basic.ipynb) | +| [Crypto](crypto-trading-basic.ipynb) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/alpacahq/alpaca-py/blob/master/examples/crypto-trading-basic.ipynb) | +| [Multi-Leg Options](options-trading-mleg.ipynb) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/alpacahq/alpaca-py/blob/master/examples/options-trading-mleg.ipynb) | diff --git a/examples/crypto-trading-basic.ipynb b/examples/crypto-trading-basic.ipynb index 26b742e2..06c208df 100644 --- a/examples/crypto-trading-basic.ipynb +++ b/examples/crypto-trading-basic.ipynb @@ -24,15 +24,14 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# 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", "\n", - "api_key = \"\"\n", - "secret_key = \"\"\n", + "api_key = None\n", + "secret_key = None\n", "\n", "#### We use paper environment for this example ####\n", "paper = True # Please do not modify this. This example is for paper trading only.\n", @@ -44,32 +43,42 @@ "trade_api_wss = None\n", "data_api_url = None\n", "stream_data_wss = None" - ] + ], + "outputs": [], + "execution_count": null }, { + "metadata": {}, "cell_type": "code", - "metadata": { - "ExecuteTime": { - "end_time": "2025-01-16T15:16:19.009655Z", - "start_time": "2025-01-16T15:16:19.007625Z" - } - }, + "source": [ + "import os\n", + "\n", + "if api_key is None:\n", + " api_key = os.environ.get('ALPACA_API_KEY')\n", + "\n", + "if secret_key is None:\n", + " secret_key = os.environ.get('ALPACA_SECRET_KEY')" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, "source": [ "# install alpaca-py if it is not available\n", "try:\n", " import alpaca\n", "except ImportError:\n", - " !pip install alpaca-py\n", + " !python3 -m pip install alpaca-py\n", " import alpaca" ], "outputs": [], - "execution_count": 13 + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "import json\n", "from datetime import datetime, timedelta\n", @@ -104,28 +113,30 @@ " QueryOrderStatus\n", ")\n", "from alpaca.common.exceptions import APIError" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# to run async code in jupyter notebook\n", "import nest_asyncio\n", "nest_asyncio.apply()" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# check version of alpaca-py\n", "alpaca.__version__" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -136,19 +147,17 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# setup clients\n", "trade_client = TradingClient(api_key=api_key, secret_key=secret_key, paper=paper, url_override=trade_api_url)" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# check trading account\n", "# You can check definition of each field in the following documents\n", @@ -156,25 +165,25 @@ "# ref. https://docs.alpaca.markets/reference/getaccount-1\n", "acct = trade_client.get_account()\n", "acct" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# check account configuration\n", "# ref. https://docs.alpaca.markets/reference/getaccountconfig-1\n", "acct_config = trade_client.get_account_configurations()\n", "acct_config" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# get list of crypto pairs\n", "# ref. https://docs.alpaca.markets/reference/get-v2-assets-1\n", @@ -184,7 +193,9 @@ ")\n", "assets = trade_client.get_all_assets(req)\n", "assets[:2]" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -195,9 +206,7 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# we will place orders which Alapca trading platform supports\n", "# - order types for crypto: market, limit, stop_limit\n", @@ -209,13 +218,13 @@ "\n", "# we will place orders for symbol: BTC/USD in this example\n", "symbol = \"BTC/USD\"" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# simple, market order\n", "# you can specify:\n", @@ -229,13 +238,13 @@ ")\n", "res = trade_client.submit_order(req)\n", "res" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# simple, market order, notional\n", "\n", @@ -248,13 +257,13 @@ ")\n", "res = trade_client.submit_order(req)\n", "res" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# simple, limit order\n", "req = LimitOrderRequest(\n", @@ -267,13 +276,13 @@ ")\n", "res = trade_client.submit_order(req)\n", "res" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# stop limit order\n", "req = StopLimitOrderRequest(\n", @@ -287,13 +296,13 @@ "\n", "res = trade_client.submit_order(req)\n", "res" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# get a list of orders including closed (e.g. filled) orders by specifying symbol\n", "req = GetOrdersRequest(\n", @@ -302,13 +311,13 @@ ")\n", "orders = trade_client.get_orders(req)\n", "orders" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# see all open orders\n", "req = GetOrdersRequest(\n", @@ -317,17 +326,19 @@ ")\n", "open_orders = trade_client.get_orders(req)\n", "open_orders" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# cancel all open orders\n", "trade_client.cancel_orders()" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -338,53 +349,51 @@ }, { "cell_type": "code", - "execution_count": 29, "metadata": {}, - "outputs": [], "source": [ "# '/' not being supported for position calls\n", "symbol=\"BTCUSD\"" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# get all open positions\n", "# ref. https://docs.alpaca.markets/reference/getallopenpositions-1\n", "positions = trade_client.get_all_positions()\n", "positions" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# get positions by symbol\n", "# ref. https://docs.alpaca.markets/reference/getopenposition-1\n", "position = trade_client.get_open_position(symbol_or_asset_id=symbol)\n", "position\n" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# get positions by asset_id\n", "trade_client.get_open_position(symbol_or_asset_id=position.asset_id)" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# close the position with specifying qty\n", "# ref. https://docs.alpaca.markets/reference/deleteopenposition-1\n", @@ -394,7 +403,9 @@ " qty = \"0.01\",\n", " )\n", ")" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -414,9 +425,7 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# subscribe trade updates\n", "trade_stream_client = TradingStream(api_key, secret_key, paper=paper, url_override = trade_api_wss)\n", @@ -426,7 +435,9 @@ "\n", "trade_stream_client.subscribe_trade_updates(trade_updates_handler)\n", "trade_stream_client.run()" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -437,21 +448,19 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# setup crypto historical data client\n", "crypto_historical_data_client = CryptoHistoricalDataClient()\n", "\n", "symbol=\"BTC/USD\"" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# get historical bars by symbol\n", "# ref. https://docs.alpaca.markets/reference/cryptobars-1\n", @@ -464,13 +473,13 @@ " limit = 2, # specify limit\n", ")\n", "crypto_historical_data_client.get_crypto_bars(req).df" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# get historical quote by symbol\n", "# ref. https://docs.alpaca.markets/reference/cryptoquotes-1\n", @@ -482,13 +491,13 @@ " limit = 2, # specify limit\n", ")\n", "crypto_historical_data_client.get_crypto_quotes(req).df" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# get historical trades by symbol\n", "req = CryptoTradesRequest(\n", @@ -498,13 +507,13 @@ " limit = 2, # specify limit\n", ")\n", "crypto_historical_data_client.get_crypto_trades(req).df" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# get latest quotes by symbol\n", "req = CryptoLatestQuoteRequest(\n", @@ -512,7 +521,9 @@ ")\n", "res = crypto_historical_data_client.get_crypto_latest_quote(req)\n", "res" - ] + ], + "outputs": [], + "execution_count": null } ], "metadata": { diff --git a/examples/options-trading-basic.ipynb b/examples/options-trading-basic.ipynb index c195bec8..c2788927 100644 --- a/examples/options-trading-basic.ipynb +++ b/examples/options-trading-basic.ipynb @@ -23,17 +23,15 @@ ] }, { - "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], + "cell_type": "code", "source": [ "# 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", "\n", - "api_key = \"\"\n", - "secret_key = \"\"\n", - "\n", + "api_key = None\n", + "secret_key = None\n", "\n", "#### We use paper environment for this example ####\n", "paper = True # Please do not modify this. This example is for paper trading only.\n", @@ -46,27 +44,42 @@ "trade_api_wss = None\n", "data_api_url = None\n", "option_stream_data_wss = None" - ] + ], + "outputs": [], + "execution_count": null }, { - "cell_type": "code", - "execution_count": null, "metadata": {}, + "cell_type": "code", + "source": [ + "import os\n", + "\n", + "if api_key is None:\n", + " api_key = os.environ.get('ALPACA_API_KEY')\n", + "\n", + "if secret_key is None:\n", + " secret_key = os.environ.get('ALPACA_SECRET_KEY')" + ], "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", "source": [ "# install alpaca-py if it is not available\n", "try:\n", " import alpaca\n", "except ImportError:\n", - " !pip install alpaca-py\n", + " !python3 -m pip install alpaca-py\n", " import alpaca" - ] + ], + "outputs": [], + "execution_count": null }, { - "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], + "cell_type": "code", "source": [ "import json\n", "from datetime import datetime, timedelta\n", @@ -102,28 +115,30 @@ " QueryOrderStatus \n", ")\n", "from alpaca.common.exceptions import APIError" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# to run async code in jupyter notebook\n", "import nest_asyncio\n", "nest_asyncio.apply()" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# check version of alpaca-py\n", "alpaca.__version__" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -134,19 +149,17 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# setup clients\n", "trade_client = TradingClient(api_key=api_key, secret_key=secret_key, paper=paper, url_override=trade_api_url)" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# check trading account\n", "# There are trhee new columns in the account object:\n", @@ -155,41 +168,41 @@ "# - options_trading_level\n", "acct = trade_client.get_account()\n", "acct" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# check account configuration\n", "# - we have new field `max_options_trading_level`\n", "acct_config = trade_client.get_account_configurations()\n", "acct_config" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# get list of assets which are options enabled\n", "# - we can filter assets by `options_enabled` attribute\n", "# - asset object has `options_enabled` attribute if it is options enabled\n", "req = GetAssetsRequest(\n", - " attributes = \"options_enabled\" \n", + " attributes = \"options_enabled\"\n", ")\n", "assets = trade_client.get_all_assets(req)\n", "assets[:2]" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# get list of options contracts for the given underlying symbol (e.g. SPY,AAPL)\n", "# - get_option_contracts() is a new method to get list of options contracts\n", @@ -212,13 +225,13 @@ ")\n", "res = trade_client.get_option_contracts(req)\n", "res" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# continue to fetch option contracts if there is next_page_token in response\n", "if res.next_page_token is not None:\n", @@ -238,38 +251,38 @@ " )\n", " res = trade_client.get_option_contracts(req)\n", " display(res)" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# get options contract by symbol\n", "# - get_option_contract() is a new method to get options contract by symbol or id\n", "symbol = res.option_contracts[0].symbol\n", "contract = trade_client.get_option_contract(symbol)\n", "contract" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# get options contract by id\n", "id = res.option_contracts[0].id\n", "contract = trade_client.get_option_contract(symbol_or_id=id)\n", "contract" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# get put options contracts\n", "underlying_symbols = [\"SPY\"]\n", @@ -295,13 +308,13 @@ ")\n", "res = trade_client.get_option_contracts(req)\n", "res.option_contracts[:2]" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# get high open_interest contract\n", "open_interest = 0\n", @@ -311,13 +324,13 @@ " open_interest = int(contract.open_interest)\n", " high_open_interest_contract = contract\n", "high_open_interest_contract" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# place buy put option order\n", "# - we can place buy put option order same as buy stock/crypto order\n", @@ -330,13 +343,13 @@ ")\n", "res = trade_client.submit_order(req)\n", "res" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# get list of orders by specifying option contract symbol\n", "req = GetOrdersRequest(\n", @@ -346,13 +359,13 @@ ")\n", "orders = trade_client.get_orders(req)\n", "orders" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# below cells should be done after market open otherwise there is no position for the option contract\n", "\n", @@ -362,53 +375,55 @@ "# please wait market open and run this example again\n", "positions = trade_client.get_all_positions()\n", "[pos for pos in positions if pos.symbol == high_open_interest_contract.symbol]" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# get positions by symbol\n", "trade_client.get_open_position(symbol_or_asset_id=high_open_interest_contract.symbol)\n" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# get positions by contract id\n", "trade_client.get_open_position(symbol_or_asset_id = high_open_interest_contract.id)" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# close the option position\n", "trade_client.close_position(\n", " symbol_or_asset_id = high_open_interest_contract.symbol,\n", " close_options = ClosePositionRequest(qty = \"1\")\n", ")" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# exercise the options position\n", "# - this method does not return anything\n", "trade_client.exercise_options_position(\n", " symbol_or_contract_id = high_open_interest_contract.symbol\n", ")" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -428,9 +443,7 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# subscribe trade updates\n", "trade_stream_client = TradingStream(api_key, secret_key, paper=paper, url_override = trade_api_wss)\n", @@ -440,7 +453,9 @@ "\n", "trade_stream_client.subscribe_trade_updates(trade_updates_handler)\n", "trade_stream_client.run()" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -451,19 +466,17 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# setup option historical data client\n", "option_historical_data_client = OptionHistoricalDataClient(api_key, secret_key, url_override = data_api_url)" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# get options historical bars by symbol\n", "req = OptionBarsRequest(\n", @@ -474,13 +487,13 @@ " limit = 2, # specify limit\n", ")\n", "option_historical_data_client.get_option_bars(req).df" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# get options historical trades by symbol\n", "req = OptionTradesRequest(\n", @@ -490,69 +503,71 @@ " limit = 2, # specify limit\n", ")\n", "option_historical_data_client.get_option_trades(req).df" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# get options exchange codes\n", "option_historical_data_client.get_option_exchange_codes()" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# get option latest quote by symbol\n", "req = OptionLatestQuoteRequest(\n", " symbol_or_symbols = [high_open_interest_contract.symbol],\n", ")\n", "option_historical_data_client.get_option_latest_quote(req)" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# get option latest trade by symbol\n", "req = OptionLatestTradeRequest(\n", " symbol_or_symbols = [high_open_interest_contract.symbol],\n", ")\n", "option_historical_data_client.get_option_latest_trade(req)" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# get option snapshot by symbol\n", "req = OptionSnapshotRequest(\n", " symbol_or_symbols = [high_open_interest_contract.symbol],\n", ")\n", "option_historical_data_client.get_option_snapshot(req)" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# get option chain by underlying_symbol\n", "req = OptionChainRequest(\n", " underlying_symbol = high_open_interest_contract.underlying_symbol,\n", ")\n", "option_historical_data_client.get_option_chain(req)" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -563,9 +578,7 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "option_data_stream_client = OptionDataStream(api_key, secret_key, url_override = option_stream_data_wss)\n", "\n", @@ -580,7 +593,9 @@ "option_data_stream_client.subscribe_trades(option_data_stream_handler, *symbols)\n", "\n", "option_data_stream_client.run()" - ] + ], + "outputs": [], + "execution_count": null } ], "metadata": { diff --git a/examples/options-trading-mleg.ipynb b/examples/options-trading-mleg.ipynb new file mode 100644 index 00000000..43ea3685 --- /dev/null +++ b/examples/options-trading-mleg.ipynb @@ -0,0 +1,567 @@ +{ + "cells": [ + { + "metadata": {}, + "cell_type": "markdown", + "source": "# Alpaca-py trading multi-leg (mleg) options", + "id": "6edd49e212343722" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/alpacahq/alpaca-py/blob/master/examples/options-trading-mleg.ipynb)", + "id": "93b07ca970df3c8" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "- This notebook shows how to use alpaca-py with options trading API endpoints to place multi-leg options orders.\n", + "- Please use ``paper account``. Please ``DO NOT`` use this notebook with live account. In this notebook, we place orders for options as an example." + ], + "id": "73e9000570f15da4" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# 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", + "\n", + "api_key = None\n", + "secret_key = None\n", + "\n", + "#### We use paper environment for this example ####\n", + "paper = True # Please do not modify this. This example is for paper trading only.\n", + "####\n", + "\n", + "# Below are the variables for development this documents\n", + "# Please do not change these variables\n", + "\n", + "trade_api_url = None" + ], + "id": "4ec3fa9be63ec8be", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "import os\n", + "\n", + "if api_key is None:\n", + " api_key = os.environ.get('ALPACA_API_KEY')\n", + "\n", + "if secret_key is None:\n", + " secret_key = os.environ.get('ALPACA_SECRET_KEY')" + ], + "id": "a95d813baede59e4", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# install alpaca-py if it is not available\n", + "try:\n", + " import alpaca\n", + "except ImportError:\n", + " !python3 -m pip install alpaca-py\n", + " import alpaca" + ], + "id": "3edec8c032b75388", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "\n", + "from datetime import datetime, timedelta\n", + "from zoneinfo import ZoneInfo\n", + "import calendar\n", + "\n", + "from alpaca.trading.client import TradingClient\n", + "\n", + "from alpaca.trading.requests import (\n", + " GetOptionContractsRequest,\n", + " MarketOrderRequest,\n", + " OptionLegRequest,\n", + " ReplaceOrderRequest,\n", + " LimitOrderRequest,\n", + ")\n", + "from alpaca.trading.enums import (\n", + " AssetStatus,\n", + " ExerciseStyle,\n", + " OrderSide,\n", + " OrderStatus,\n", + " TimeInForce,\n", + " OrderClass,\n", + " ContractType\n", + ")\n", + "from alpaca.common.exceptions import APIError" + ], + "id": "8537217fbfb2d777", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## Trading Client", + "id": "8d19088b41288688" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# setup client\n", + "trade_client = TradingClient(api_key=api_key, secret_key=secret_key, paper=paper, url_override=trade_api_url)" + ], + "id": "cf259f6b53acffe2", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# First, let us see if the account is enabled for multi-leg options trading\n", + "acct = trade_client.get_account()\n", + "if acct.options_trading_level >= 3:\n", + " print(\"Account is enabled for multi-leg options trading!\")\n", + "else:\n", + " print(\"Account is not enabled for multi-leg options trading!\")" + ], + "id": "321926acf817f87e", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### The Straddle\n", + "\n", + "A straddle is an options strategy that involves buying both a call and a put option with the same strike price and expiration date. This is typically done when the trader expects a large price movement but is unsure of the direction. That is to say, the trader will profit if the price moves significantly in either direction from the strike price. By using a multi-leg order, rather than place two separate orders, the trader can ensure that both legs\n", + "of the order are executed simultaneously." + ], + "id": "23e5cccd91409d73" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# Let us find the earnings date for TSLA closest to today\n", + "# Typically these occur OCT, JAN, APR, or JUL around the middle of the 3rd week of the month\n", + "\n", + "\n", + "today = datetime.now(tz=ZoneInfo(\"America/New_York\"))\n", + "\n", + "earnings_month = None\n", + "earnings_day = None\n", + "earnings_year = today.year\n", + "\n", + "# find next earnings date month\n", + "if today.month < 4:\n", + " earnings_month = 4\n", + "elif today.month < 7:\n", + " earnings_month = 7\n", + "elif today.month < 10:\n", + " earnings_month = 10\n", + "else:\n", + " earnings_year += 1\n", + " earnings_month = 1\n", + "\n", + "# find wednesday of the 3rd week of the month\n", + "earnings_day = calendar.monthcalendar(earnings_year, earnings_month)[-2][calendar.WEDNESDAY]\n", + "\n", + "earnings_date = datetime(earnings_year, earnings_month, earnings_day)\n", + "\n", + "earnings_date" + ], + "id": "1b7cfa90b85495f9", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# This is a function that will return a contract which minimizes the difference from a target price\n", + "def find_nearest_strike_contract(contracts, target_price):\n", + " min_diff = 0\n", + " min_contract = None\n", + " for contract in contracts:\n", + " diff = abs(float(contract.strike_price) - target_price)\n", + " if min_contract is None or diff < min_diff:\n", + " min_diff = diff\n", + " min_contract = contract\n", + " return min_contract" + ], + "id": "15c5b3f0e3600865", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# Let us find a call and put option for TSLA with the closest expiration date to the earnings date\n", + "# and a strike price closest to some price\n", + "order_legs = []\n", + "optimal_price = 413.82 # this could be our entry price, for example, that we wish to hedge against (if we expect volatility)\n", + "\n", + "for c_type in [ContractType.CALL, ContractType.PUT]:\n", + " req = GetOptionContractsRequest(\n", + " underlying_symbols=[\"TSLA\"],\n", + " status=AssetStatus.ACTIVE,\n", + " expiration_date_gte=earnings_date,\n", + " expiration_date_lte=earnings_date + timedelta(weeks=4),\n", + " style=ExerciseStyle.AMERICAN,\n", + " strike_price_gte=str(optimal_price - 20),\n", + " strike_price_lte=str(optimal_price + 20),\n", + " limit=10,\n", + " type=c_type, # We could do this in one request, without setting type, if we set up the\n", + " # gte, lte, and limit strategically (but the current approach is a bit more generalizable).\n", + " # See the next example for how to do this.\n", + " page_token=None,\n", + " )\n", + " cts = trade_client.get_option_contracts(req)\n", + "\n", + " c = find_nearest_strike_contract(cts.option_contracts, optimal_price)\n", + " order_legs.append(OptionLegRequest(\n", + " symbol=c.symbol,\n", + " side=OrderSide.BUY,\n", + " ratio_qty=1\n", + " ))\n", + "\n", + "# 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" + ], + "id": "c5d3eef0fcabc15e", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# Now we place the order for both legs at the same time\n", + "req = MarketOrderRequest(\n", + " qty=1,\n", + " order_class=OrderClass.MLEG,\n", + " time_in_force=TimeInForce.DAY,\n", + " legs=order_legs\n", + ")\n", + "res = trade_client.submit_order(req)\n", + "res" + ], + "id": "c0597fc0c1f6098f", + "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", + "source": [ + "### The Iron Condor\n", + "\n", + "This is traditionally harder to implement because it requires 4 legs. However, with multi-leg orders, Alpaca makes doing this a breeze! Moreover, we can use a limit order on the multi-leg order to better time our entry (which is much more important with an iron condor than with a straddle).\n", + "\n", + "This time we are considering 4 separate strike prices: A, B, C, and D. We buy a put at A, sell a put at B, sell a call at C, and buy a call at D. A trader expects the price to stay within the range of B and C and will typically enter only once the price is between B and C. Using a limit order, we execute the order when the *net* price is 0 or better.\n", + "\n" + ], + "id": "42bb8487b4414977" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# At the time of writing, TSLA has a standard deviation (a common measure of variation for the price) of 5.02 and a price of 413.82\n", + "# Let's create a interval around the current price such that B and C are a standard deviation away from each other\n", + "\n", + "stddev = 5.02\n", + "B = optimal_price - (stddev / 2)\n", + "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", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# Now, instead of doing two separate requests, we can do one request to get all the contracts we need\n", + "# We set this up by setting the gte, lte, and limit strategically\n", + "# That being said, it is still safest to do separate requests as we did in the prior example as the number of contracts\n", + "# returned can vary based on the strike prices and expiration dates (which are not fixed in this notebook).\n", + "\n", + "# Let's find the delta between strike prices from our prior example\n", + "max_delta = 0\n", + "for i in range(len(cts.option_contracts) - 1):\n", + " delta = abs(float(cts.option_contracts[i].strike_price) - float(cts.option_contracts[i + 1].strike_price))\n", + " if delta > max_delta:\n", + " max_delta = delta\n", + "\n", + "\n", + "def next_divisible(n: float, div: float, round_up: bool):\n", + " if n % div == 0:\n", + " return n\n", + " if round_up:\n", + " return n + div - n % div\n", + " return n - n % div\n", + "\n", + "\n", + "min_contract_price = next_divisible(A, max_delta, False)\n", + "max_contract_price = next_divisible(D, max_delta, True)\n", + "print(f\"Min: {min_contract_price}, Max: {max_contract_price}\")" + ], + "id": "700d60b9b3968156", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# Now the actual request\n", + "req = GetOptionContractsRequest(\n", + " underlying_symbols=[\"TSLA\"],\n", + " status=AssetStatus.ACTIVE,\n", + " expiration_date_gte=cts.option_contracts[0].expiration_date - timedelta(weeks=5),\n", + " expiration_date_lte=cts.option_contracts[0].expiration_date - timedelta(weeks=3),\n", + " style=ExerciseStyle.AMERICAN,\n", + " strike_price_gte=str(min_contract_price),\n", + " strike_price_lte=str(max_contract_price),\n", + " page_token=None,\n", + ")\n", + "cts_m = trade_client.get_option_contracts(req)\n", + "cts_m" + ], + "id": "4e477db0a6bc1748", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# This finds contracts which minimizes the difference from a set of target prices\n", + "class ContractBuffer:\n", + " def __init__(self, optimal, is_call, is_buy):\n", + " self.optimal = optimal\n", + " self.is_call = is_call\n", + " self.is_buy = is_buy\n", + " self.contract = None\n", + " self.diff = 0\n", + "\n", + " def __repr__(self):\n", + " return f\"Contract: {self.contract}, Optimal: {self.optimal}, Diff: {self.diff}, Is Call: {self.is_call}\"\n", + "\n", + "\n", + "buffers = [ContractBuffer(A, False, True), ContractBuffer(B, False, False), ContractBuffer(C, True, False),\n", + " ContractBuffer(D, True, True)]\n", + "\n", + "for contract in cts_m.option_contracts:\n", + " for buff in buffers:\n", + " is_call = contract.type == ContractType.CALL\n", + " if buff.is_call != is_call:\n", + " continue\n", + " diff = abs(float(contract.strike_price) - buff.optimal)\n", + " if diff < buff.diff or buff.contract is None:\n", + " buff.diff = diff\n", + " buff.contract = contract\n", + "\n", + "order_legs_m = []\n", + "for buff in buffers:\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", + " ))\n", + "\n", + "del buffers\n", + "order_legs_m" + ], + "id": "c4d6a9949d7fed", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# Order for the iron condor\n", + "req = LimitOrderRequest(\n", + " qty=50,\n", + " order_class=OrderClass.MLEG,\n", + " time_in_force=TimeInForce.DAY,\n", + " legs=order_legs_m,\n", + " limit_price=0 # i.e., for a net price of 0\n", + ")\n", + "res = trade_client.submit_order(req)\n", + "res" + ], + "id": "d54598897e5d0453", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## Querying, Replacing, and Cancelling Multi-leg Orders", + "id": "54ca6690fdc6f747" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# We have two resolutions at which we can query the order status: by the mleg's id or by the individual legs' ids.\n", + "\n", + "# Query by the order's id\n", + "q1 = trade_client.get_order_by_client_id(res.client_order_id)\n", + "\n", + "# Query just the first leg's id\n", + "q2 = trade_client.get_order_by_client_id(res.legs[0].client_order_id)\n", + "\n", + "print(f\"Query by whole order id: {q1}\\n===\\nQuery by leg id: {q2}\")" + ], + "id": "f889a157aaf2ad2e", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# Note that we cannot cancel nor replace an individual leg\n", + "try:\n", + " if q1.legs[0].status != OrderStatus.FILLED:\n", + " res = trade_client.cancel_order_by_id(res.legs[0].id)\n", + " print(f\"Cancelled leg: {res}\")\n", + " else:\n", + " print(\"Order is already filled.\")\n", + "except APIError as e:\n", + " print(f\"Error: {e}\")\n", + "\n", + "try:\n", + " if q1.status != OrderStatus.FILLED:\n", + " req = ReplaceOrderRequest(\n", + " qty=1\n", + " )\n", + " res = trade_client.replace_order_by_id(res.legs[0].id, req)\n", + " print(f\"Replaced order: {res}\")\n", + " else:\n", + " print(\"Order is already filled.\")\n", + "except APIError as e:\n", + " print(f\"Error: {e}\")\n", + "\n", + "# Should get an output of form\n", + "# Error: {\"code\":42210000,\"message\":\"cannot cancel individual legs of a mleg order\"}\n", + "# Error: {\"code\":42210000,\"message\":\"cannot replace individual legs of a mleg order\"}" + ], + "id": "e3ca89c326f2b541", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# Replace overall order\n", + "if q1.status != OrderStatus.FILLED:\n", + " # We can replace the order\n", + " req = ReplaceOrderRequest(\n", + " qty=55,\n", + " )\n", + " res = trade_client.replace_order_by_id(res.id, req)\n", + " print(f\"Replaced order: {res}\")\n", + "else:\n", + " print(\"Order is already filled.\")" + ], + "id": "34df30f717d2683a", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# Cancel the whole order\n", + "trade_client.cancel_order_by_id(res.id)" + ], + "id": "8cc9815f84b7b49", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# To validate, we can query the order again\n", + "res = trade_client.get_order_by_client_id(res.client_order_id)\n", + "res" + ], + "id": "cefb7d7785ba13ad", + "outputs": [], + "execution_count": null + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/stocks-trading-basic.ipynb b/examples/stocks-trading-basic.ipynb index 1d09f14a..8d82d581 100644 --- a/examples/stocks-trading-basic.ipynb +++ b/examples/stocks-trading-basic.ipynb @@ -23,18 +23,19 @@ ] }, { + "metadata": { + "jupyter": { + "is_executing": true + } + }, "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], "source": [ - "import os\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", - "# Alternatively, you can set the APCA_API_KEY_ID and APCA_API_SECRET_KEY environment variables\n", - "api_key = os.getenv(\"APCA_API_KEY_ID\")\n", - "secret_key = os.getenv(\"APCA_API_SECRET_KEY\")\n", + "\n", + "api_key = None\n", + "secret_key = None\n", "\n", "#### We use paper environment for this example ####\n", "paper = True # Please do not modify this. This example is for paper trading only.\n", @@ -46,27 +47,42 @@ "trade_api_wss = None\n", "data_api_url = None\n", "stream_data_wss = None" - ] + ], + "outputs": [], + "execution_count": null }, { - "cell_type": "code", - "execution_count": null, "metadata": {}, + "cell_type": "code", + "source": [ + "import os\n", + "\n", + "if api_key is None:\n", + " api_key = os.environ.get('ALPACA_API_KEY')\n", + "\n", + "if secret_key is None:\n", + " secret_key = os.environ.get('ALPACA_SECRET_KEY')" + ], "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", "source": [ "# install alpaca-py if it is not available\n", "try:\n", " import alpaca\n", "except ImportError:\n", - " !pip install alpaca-py\n", + " !python3 -m pip install alpaca-py\n", " import alpaca" - ] + ], + "outputs": [], + "execution_count": null }, { - "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], + "cell_type": "code", "source": [ "from datetime import datetime, timedelta\n", "from zoneinfo import ZoneInfo\n", @@ -105,28 +121,30 @@ " QueryOrderStatus,\n", " TimeInForce,\n", ")" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# to run async code in jupyter notebook\n", "import nest_asyncio\n", "nest_asyncio.apply()" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# check version of alpaca-py\n", "alpaca.__version__" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -137,19 +155,17 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# setup clients\n", "trade_client = TradingClient(api_key=api_key, secret_key=secret_key, paper=paper, url_override=trade_api_url)" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# check trading account\n", "# You can check definition of each field in the following documents\n", @@ -157,25 +173,25 @@ "# ref. https://docs.alpaca.markets/reference/getaccount-1\n", "acct = trade_client.get_account()\n", "acct" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# check account configuration\n", "# ref. https://docs.alpaca.markets/reference/getaccountconfig-1\n", "acct_config = trade_client.get_account_configurations()\n", "acct_config" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# set account configuration\n", "# ref. https://docs.alpaca.markets/reference/patchaccountconfig-1\n", @@ -189,13 +205,13 @@ "req.fractional_trading = not req.fractional_trading # toggle fractional trading\n", "acct_config_reverted = trade_client.set_account_configurations(req)\n", "display(acct_config_reverted)" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# get list of assets which are us_equity (default), active, and in NASDAQ\n", "# ref. https://docs.alpaca.markets/reference/get-v2-assets-1\n", @@ -206,7 +222,9 @@ ")\n", "assets = trade_client.get_all_assets(req)\n", "assets[:2]" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -217,9 +235,7 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# we will place orders which Alapca trading platform supports\n", "# - order classes: simple, bracket, oco, oto\n", @@ -234,13 +250,13 @@ "\n", "# we will place orders for symbol: SPY in this example\n", "symbol = \"SPY\"" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# simple, market order, fractional qty\n", "# Alpaca trading platform support fractional trading by default\n", @@ -259,13 +275,13 @@ ")\n", "res = trade_client.submit_order(req)\n", "res" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# simple, market order, notional\n", "# Alpaca trading platform support fractional trading by default\n", @@ -281,13 +297,13 @@ ")\n", "res = trade_client.submit_order(req)\n", "res" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# simple, limit order, fractional qty\n", "req = LimitOrderRequest(\n", @@ -300,13 +316,13 @@ ")\n", "res = trade_client.submit_order(req)\n", "res" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# stop order\n", "req = StopOrderRequest(\n", @@ -319,13 +335,13 @@ "\n", "res = trade_client.submit_order(req)\n", "res" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# stop limit order\n", "req = StopLimitOrderRequest(\n", @@ -339,13 +355,13 @@ "\n", "res = trade_client.submit_order(req)\n", "res" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# bracket order with both stop loss and take profit\n", "req = MarketOrderRequest(\n", @@ -359,13 +375,13 @@ ")\n", "res = trade_client.submit_order(req)\n", "res" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# oto order with stop loss\n", "req = LimitOrderRequest(\n", @@ -380,13 +396,13 @@ "\n", "res = trade_client.submit_order(req)\n", "res" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# oco limit order\n", "req = LimitOrderRequest(\n", @@ -400,13 +416,13 @@ "\n", "res = trade_client.submit_order(req)\n", "res" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# trailing stop order\n", "req = TrailingStopOrderRequest(\n", @@ -419,13 +435,13 @@ "\n", "res = trade_client.submit_order(req)\n", "res" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# get a list of orders including closed (e.g. filled) orders by specifying symbol\n", "req = GetOrdersRequest(\n", @@ -434,13 +450,13 @@ ")\n", "orders = trade_client.get_orders(req)\n", "orders" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# see all open orders\n", "req = GetOrdersRequest(\n", @@ -449,17 +465,19 @@ ")\n", "open_orders = trade_client.get_orders(req)\n", "open_orders" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# cancel all open orders\n", "trade_client.cancel_orders()" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -470,43 +488,41 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# get all open positions\n", "# ref. https://docs.alpaca.markets/reference/getallopenpositions-1\n", "positions = trade_client.get_all_positions()\n", "positions" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# get positions by symbol\n", "# ref. https://docs.alpaca.markets/reference/getopenposition-1\n", "position = trade_client.get_open_position(symbol_or_asset_id=symbol)\n", "position\n" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# get positions by asset_id\n", "trade_client.get_open_position(symbol_or_asset_id=position.asset_id)" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# close the position with specifying qty\n", "# ref. https://docs.alpaca.markets/reference/deleteopenposition-1\n", @@ -516,7 +532,9 @@ " qty = \"0.01\",\n", " )\n", ")" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -536,9 +554,7 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# subscribe trade updates\n", "trade_stream_client = TradingStream(api_key, secret_key, paper=paper, url_override = trade_api_wss)\n", @@ -548,7 +564,9 @@ "\n", "trade_stream_client.subscribe_trade_updates(trade_updates_handler)\n", "trade_stream_client.run()" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -559,19 +577,17 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# setup stock historical data client\n", "stock_historical_data_client = StockHistoricalDataClient(api_key, secret_key, url_override = data_api_url)" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# get historical bars by symbol\n", "# ref. https://docs.alpaca.markets/reference/stockbars-1\n", @@ -584,13 +600,13 @@ " limit = 2, # specify limit\n", ")\n", "stock_historical_data_client.get_stock_bars(req).df" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# get historical trades by symbol\n", "req = StockTradesRequest(\n", @@ -600,13 +616,13 @@ " limit = 2, # specify limit\n", ")\n", "stock_historical_data_client.get_stock_trades(req).df" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# get historical quotes by symbol\n", "req = StockQuotesRequest(\n", @@ -616,13 +632,13 @@ " limit = 2, # specify limit\n", ")\n", "stock_historical_data_client.get_stock_quotes(req).df" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# get latest quotes by symbol\n", "req = StockQuotesRequest(\n", @@ -630,7 +646,9 @@ ")\n", "res = stock_historical_data_client.get_stock_latest_quote(req)\n", "res" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -641,9 +659,7 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "stock_data_stream_client = StockDataStream(api_key, secret_key, url_override = stream_data_wss)\n", "\n", @@ -656,7 +672,9 @@ "stock_data_stream_client.subscribe_trades(stock_data_stream_handler, *symbols)\n", "\n", "stock_data_stream_client.run()" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -667,16 +685,16 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "corporate_actions_client = CorporateActionsClient(api_key, secret_key)\n", "corporate_actions_client.get_corporate_actions(CorporateActionsRequest(\n", " start=datetime(2020, 1, 1),\n", " symbols=[symbol]\n", ")).df" - ] + ], + "outputs": [], + "execution_count": null } ], "metadata": { 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 de35157e..74223fc3 100644 --- a/tests/trading/trading_client/test_order_routes.py +++ b/tests/trading/trading_client/test_order_routes.py @@ -1,3 +1,4 @@ +import warnings from uuid import UUID import pytest @@ -18,6 +19,7 @@ GetOrdersRequest, LimitOrderRequest, MarketOrderRequest, + OptionLegRequest, ReplaceOrderRequest, StopLossRequest, TakeProfitRequest, @@ -622,3 +624,124 @@ def test_order_position_intent(reqmock, trading_client: TradingClient): lo_response = trading_client.submit_order(lo) assert lo_response.status == OrderStatus.ACCEPTED + + +def test_mleg_request_validation() -> 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): + # 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 + ), + ], + ) + + with pytest.raises(ValueError): + # 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) + ], + ) + + with pytest.raises(ValueError): + # 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 + ], + ) + + with pytest.raises(ValueError): + # Missing legs + o_req(qty=1, time_in_force=TimeInForce.DAY, order_class=OrderClass.MLEG) + + with pytest.raises(ValueError): + # 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 + ), + ], + ) + + with pytest.raises(ValueError): + # 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) + ], + )