From b554eb87f9a74b9246804c8677cfb35e20c43013 Mon Sep 17 00:00:00 2001 From: Mate Budai Date: Mon, 27 May 2024 13:24:47 +0200 Subject: [PATCH] feat: Extend Order API with commission_type --- alpaca/broker/enums.py | 11 ++ alpaca/broker/requests.py | 12 ++ docs/api_reference/broker/enums.rst | 5 + poetry.lock | 51 ++--- .../test_order_commission_routes.py | 182 ++++++++++++++++++ 5 files changed, 236 insertions(+), 25 deletions(-) create mode 100644 tests/broker/broker_client/test_order_commission_routes.py diff --git a/alpaca/broker/enums.py b/alpaca/broker/enums.py index 16216fdf..02c8dfe0 100644 --- a/alpaca/broker/enums.py +++ b/alpaca/broker/enums.py @@ -426,3 +426,14 @@ class JournalStatus(str, Enum): REFUSED = "refused" CORRECT = "correct" DELETED = "deleted" + + +class CommissionType(str, Enum): + """ + Represents the available ways of charging commission. This determines how + the value in the commission field is interpreted. + """ + + NOTIONAL = "notional" + BPS = "bps" + QTY = "qty" diff --git a/alpaca/broker/requests.py b/alpaca/broker/requests.py index dec9c3e2..6a78bba0 100644 --- a/alpaca/broker/requests.py +++ b/alpaca/broker/requests.py @@ -32,6 +32,7 @@ VisaType, JournalEntryType, JournalStatus, + CommissionType, ) from alpaca.common.enums import Sort, SupportedCurrencies from alpaca.trading.enums import ActivityType, AccountStatus, OrderType, AssetClass @@ -662,6 +663,7 @@ class OrderRequest(BaseOrderRequest): 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. commission (Optional[float]): The dollar value commission you want to charge the end user. + commission_type (Optional[CommissionType]): An enum to select how to interpret the value provided in the commission field: notional, bps, qty. """ commission: Optional[float] = None @@ -705,9 +707,11 @@ class MarketOrderRequest(BaseMarketOrderRequest): 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. commission (Optional[float]): The dollar value commission you want to charge the end user. + commission_type (Optional[CommissionType]): An enum to select how to interpret the value provided in the commission field: notional, bps, qty. """ commission: Optional[float] = None + commission_type: Optional[CommissionType] = None class LimitOrderRequest(BaseLimitOrderRequest): @@ -729,9 +733,11 @@ class LimitOrderRequest(BaseLimitOrderRequest): stop_loss (Optional[StopLossRequest]): For orders with multiple legs, an order to exit a losing trade. limit_price (float): The worst fill price for a limit or stop limit order. commission (Optional[float]): The dollar value commission you want to charge the end user. + commission_type (Optional[CommissionType]): An enum to select how to interpret the value provided in the commission field: notional, bps, qty. """ commission: Optional[float] = None + commission_type: Optional[CommissionType] = None class StopOrderRequest(BaseStopOrderRequest): @@ -754,9 +760,11 @@ class StopOrderRequest(BaseStopOrderRequest): 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. commission (Optional[float]): The dollar value commission you want to charge the end user. + commission_type (Optional[CommissionType]): An enum to select how to interpret the value provided in the commission field: notional, bps, qty. """ commission: Optional[float] = None + commission_type: Optional[CommissionType] = None class StopLimitOrderRequest(BaseStopLimitOrderRequest): @@ -780,9 +788,11 @@ class StopLimitOrderRequest(BaseStopLimitOrderRequest): order is converted to a limit order. limit_price (float): The worst fill price for a limit or stop limit order. commission (Optional[float]): The dollar value commission you want to charge the end user + commission_type (Optional[CommissionType]): An enum to select how to interpret the value provided in the commission field: notional, bps, qty. """ commission: Optional[float] = None + commission_type: Optional[CommissionType] = None class TrailingStopOrderRequest(BaseTrailingStopOrderRequest): @@ -805,9 +815,11 @@ class TrailingStopOrderRequest(BaseTrailingStopOrderRequest): trail_price (Optional[float]): The absolute price difference by which the trailing stop will trail. trail_percent (Optional[float]): The percent price difference by which the trailing stop will trail. commission (Optional[float]): The dollar value commission you want to charge the end user. + commission_type (Optional[CommissionType]): An enum to select how to interpret the value provided in the commission field: notional, bps, qty. """ commission: Optional[float] = None + commission_type: Optional[CommissionType] = None class CancelOrderResponse(BaseCancelOrderResponse): diff --git a/docs/api_reference/broker/enums.rst b/docs/api_reference/broker/enums.rst index 3be550c0..3764aae1 100644 --- a/docs/api_reference/broker/enums.rst +++ b/docs/api_reference/broker/enums.rst @@ -136,3 +136,8 @@ JournalStatus ------------- .. autoenum:: alpaca.broker.enums.JournalStatus + +CommissionType +---------------------- + +.. autoenum:: alpaca.broker.enums.CommissionType diff --git a/poetry.lock b/poetry.lock index 29891be0..2a38593b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "alabaster" @@ -115,33 +115,33 @@ lxml = ["lxml"] [[package]] name = "black" -version = "24.3.0" +version = "24.4.2" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" files = [ - {file = "black-24.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395"}, - {file = "black-24.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995"}, - {file = "black-24.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7"}, - {file = "black-24.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0"}, - {file = "black-24.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9"}, - {file = "black-24.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597"}, - {file = "black-24.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d"}, - {file = "black-24.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5"}, - {file = "black-24.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f"}, - {file = "black-24.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11"}, - {file = "black-24.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4"}, - {file = "black-24.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5"}, - {file = "black-24.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837"}, - {file = "black-24.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd"}, - {file = "black-24.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213"}, - {file = "black-24.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959"}, - {file = "black-24.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb"}, - {file = "black-24.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7"}, - {file = "black-24.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7"}, - {file = "black-24.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f"}, - {file = "black-24.3.0-py3-none-any.whl", hash = "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93"}, - {file = "black-24.3.0.tar.gz", hash = "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f"}, + {file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"}, + {file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"}, + {file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"}, + {file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"}, + {file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"}, + {file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"}, + {file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"}, + {file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"}, + {file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"}, + {file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"}, + {file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"}, + {file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"}, + {file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"}, + {file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"}, + {file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"}, + {file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"}, + {file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"}, + {file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"}, + {file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"}, + {file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"}, + {file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"}, + {file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"}, ] [package.dependencies] @@ -886,8 +886,8 @@ files = [ [package.dependencies] numpy = [ {version = ">=1.20.3", markers = "python_version < \"3.10\""}, - {version = ">=1.21.0", markers = "python_version >= \"3.10\" and python_version < \"3.11\""}, {version = ">=1.23.2", markers = "python_version >= \"3.11\""}, + {version = ">=1.21.0", markers = "python_version >= \"3.10\" and python_version < \"3.11\""}, ] python-dateutil = ">=2.8.2" pytz = ">=2020.1" @@ -1172,6 +1172,7 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, diff --git a/tests/broker/broker_client/test_order_commission_routes.py b/tests/broker/broker_client/test_order_commission_routes.py new file mode 100644 index 00000000..a35834b8 --- /dev/null +++ b/tests/broker/broker_client/test_order_commission_routes.py @@ -0,0 +1,182 @@ +from alpaca.broker.client import BrokerClient +from alpaca.common.enums import BaseURL +from alpaca.broker.enums import CommissionType +from alpaca.trading.enums import OrderSide, OrderStatus, TimeInForce +from alpaca.broker.requests import ( + MarketOrderRequest, +) + + +def test_order_commission_type(reqmock, client: BrokerClient): + account_id = "0d969814-40d6-4b2b-99ac-2e37427f1ad2" + + # 1. commission_type notional per order + reqmock.post( + f"{BaseURL.BROKER_SANDBOX.value}/v1/trading/accounts/{account_id}/orders", + text=""" + { + "id": "61e69015-8549-4bfd-b9c3-01e75843f47d", + "client_order_id": "eb9e2aaa-f71a-4f51-b5b4-52a6c565dad4", + "created_at": "2021-03-16T18:38:01.942282Z", + "updated_at": "2021-03-16T18:38:01.942282Z", + "submitted_at": "2021-03-16T18:38:01.937734Z", + "filled_at": null, + "expired_at": null, + "canceled_at": null, + "failed_at": null, + "replaced_at": null, + "replaced_by": null, + "replaces": null, + "asset_id": "b4695157-0d1d-4da0-8f9e-5c53149389e4", + "symbol": "SPY`", + "asset_class": "us_equity", + "notional": null, + "qty": 1, + "filled_qty": "0", + "filled_avg_price": null, + "order_class": "simple", + "order_type": "market", + "type": "market", + "side": "buy", + "time_in_force": "day", + "limit_price": null, + "stop_price": null, + "status": "accepted", + "extended_hours": false, + "legs": null, + "trail_percent": null, + "trail_price": null, + "hwm": null, + "commission": 1.25, + "commission_type": "notional" + } + """, + ) + + mo = MarketOrderRequest( + symbol="SPY", + side=OrderSide.BUY, + time_in_force=TimeInForce.DAY, + qty=1, + commission_type=CommissionType.NOTIONAL, + ) + + assert mo.commission_type == CommissionType.NOTIONAL + + mo_response = client.submit_order_for_account(account_id, mo) + + assert mo_response.status == OrderStatus.ACCEPTED + + # 2. commission_type bps + reqmock.post( + f"{BaseURL.BROKER_SANDBOX.value}/v1/trading/accounts/{account_id}/orders", + text=""" + { + "id": "61e69015-8549-4bfd-b9c3-01e75843f47d", + "client_order_id": "eb9e2aaa-f71a-4f51-b5b4-52a6c565dad4", + "created_at": "2021-03-16T18:38:01.942282Z", + "updated_at": "2021-03-16T18:38:01.942282Z", + "submitted_at": "2021-03-16T18:38:01.937734Z", + "filled_at": null, + "expired_at": null, + "canceled_at": null, + "failed_at": null, + "replaced_at": null, + "replaced_by": null, + "replaces": null, + "asset_id": "b4695157-0d1d-4da0-8f9e-5c53149389e4", + "symbol": "SPY`", + "asset_class": "us_equity", + "notional": null, + "qty": 1, + "filled_qty": "0", + "filled_avg_price": null, + "order_class": "simple", + "order_type": "market", + "type": "market", + "side": "buy", + "time_in_force": "day", + "limit_price": null, + "stop_price": null, + "status": "accepted", + "extended_hours": false, + "legs": null, + "trail_percent": null, + "trail_price": null, + "hwm": null, + "commission": 1.25, + "commission_type": "bps" + } + """, + ) + + mo = MarketOrderRequest( + symbol="SPY", + side=OrderSide.BUY, + time_in_force=TimeInForce.DAY, + qty=1, + commission_type=CommissionType.BPS, + ) + + assert mo.commission_type == CommissionType.BPS + + mo_response = client.submit_order_for_account(account_id, mo) + + assert mo_response.status == OrderStatus.ACCEPTED + + # 3. commission_type per qty + reqmock.post( + f"{BaseURL.BROKER_SANDBOX.value}/v1/trading/accounts/{account_id}/orders", + text=""" + { + "id": "61e69015-8549-4bfd-b9c3-01e75843f47d", + "client_order_id": "eb9e2aaa-f71a-4f51-b5b4-52a6c565dad4", + "created_at": "2021-03-16T18:38:01.942282Z", + "updated_at": "2021-03-16T18:38:01.942282Z", + "submitted_at": "2021-03-16T18:38:01.937734Z", + "filled_at": null, + "expired_at": null, + "canceled_at": null, + "failed_at": null, + "replaced_at": null, + "replaced_by": null, + "replaces": null, + "asset_id": "b4695157-0d1d-4da0-8f9e-5c53149389e4", + "symbol": "SPY`", + "asset_class": "us_equity", + "notional": null, + "qty": 3, + "filled_qty": "0", + "filled_avg_price": null, + "order_class": "simple", + "order_type": "market", + "type": "market", + "side": "buy", + "time_in_force": "day", + "limit_price": null, + "stop_price": null, + "status": "accepted", + "extended_hours": false, + "legs": null, + "trail_percent": null, + "trail_price": null, + "hwm": null, + "commission": 1.25, + "commission_type": "bps" + } + """, + ) + + mo = MarketOrderRequest( + symbol="SPY", + side=OrderSide.BUY, + time_in_force=TimeInForce.DAY, + qty=1, + commission_type=CommissionType.QTY, + ) + + assert mo.commission_type == CommissionType.QTY + + mo_response = client.submit_order_for_account(account_id, mo) + + assert mo_response.status == OrderStatus.ACCEPTED