diff --git a/cschwabpy/SchwabAsyncClient.py b/cschwabpy/SchwabAsyncClient.py index 2eff209..f43b353 100644 --- a/cschwabpy/SchwabAsyncClient.py +++ b/cschwabpy/SchwabAsyncClient.py @@ -32,6 +32,8 @@ import base64 import json +HEADER_ORDER_ID_PATTERN = re.compile(r"orders/(\d+)") + class SchwabAsyncClient(object): def __init__( @@ -213,7 +215,8 @@ async def get_instruments_async( async def place_order_async( self, account_number_hash: AccountNumberWithHashID, order: Order - ) -> bool: + ) -> int: + """Place an order (Equity or Option) for a specific account, returns order id (int).""" await self._ensure_valid_access_token() target_url = f"{SCHWAB_TRADER_API_BASE_URL}/accounts/{account_number_hash.hashValue}/orders" client = httpx.AsyncClient() if self.__client is None else self.__client @@ -226,9 +229,15 @@ async def place_order_async( headers=_header, ) if response.status_code == 201: - return True - else: - raise Exception("Failed to place order. Status: ", response.status_code) + location_url = response.headers.get("Location") + if location_url is not None: + needle = re.search(HEADER_ORDER_ID_PATTERN, location_url) + if needle: + return int(needle.group(1)) + else: + raise Exception("Failed to locate order ID in response") + + raise Exception("Failed to place order. Status: ", response.status_code) finally: if not self.__keep_client_alive: await client.aclose() diff --git a/cschwabpy/SchwabClient.py b/cschwabpy/SchwabClient.py index ebc9804..e2762fb 100644 --- a/cschwabpy/SchwabClient.py +++ b/cschwabpy/SchwabClient.py @@ -32,6 +32,8 @@ import base64 import json +HEADER_ORDER_ID_PATTERN = re.compile(r"orders/(\d+)") + class SchwabClient(object): """This is regular sync client. For async client, use SchwabClientAsync.""" @@ -215,7 +217,7 @@ def get_instruments( def place_order( self, account_number_hash: AccountNumberWithHashID, order: Order - ) -> bool: + ) -> int: self._ensure_valid_access_token() target_url = f"{SCHWAB_TRADER_API_BASE_URL}/accounts/{account_number_hash.hashValue}/orders" client = httpx.Client() if self.__client is None else self.__client @@ -229,9 +231,15 @@ def place_order( headers=_header, ) if response.status_code == 201: - return True - else: - raise Exception("Failed to place order. Status: ", response.status_code) + location_url = response.headers.get("Location") + if location_url is not None: + needle = re.search(HEADER_ORDER_ID_PATTERN, location_url) + if needle: + return int(needle.group(1)) + else: + raise Exception("Failed to locate order ID in response") + + raise Exception("Failed to place order. Status: ", response.status_code) finally: if not self.__keep_client_alive: client.close() diff --git a/pyproject.toml b/pyproject.toml index 27ab06b..e0ded68 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "cschwabpy" -version = "0.1.1" +version = "0.1.2" description = "" authors = ["Tony Wang "] readme = "README.md" diff --git a/setup.py b/setup.py index 33a4d00..b45bf56 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="CSchwabPy", - version="0.1.1", + version="0.1.2", description="Charles Schwab Stock & Option Trade API Client for Python.", long_description=long_description, long_description_content_type="text/markdown", diff --git a/tests/test_models.py b/tests/test_models.py index 7a3fc4e..95d77c3 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -13,13 +13,26 @@ OptionContractStrategy, ) from cschwabpy.models.trade_models import ( - SecuritiesAccount, AccountNumberWithHashID, - MarginAccount, - CashAccount, - AccountType, Order, + ExecutionLeg, + ExecutionType, + OrderLeg, + PositionEffect, + OrderLegInstruction, + AssetType, + OrderLegCollection, + AccountInstrument, + OrderActivity, + ComplexOrderStrategyType, + Session, + Duration, + OrderType, OrderStatus, + OrderStrategyType, + InstrumentProjection, + AccountType, + SecuritiesAccount, ) from cschwabpy.models.token import Tokens, LocalTokenStore from cschwabpy.SchwabAsyncClient import SchwabAsyncClient @@ -142,6 +155,85 @@ async def test_get_order(httpx_mock: HTTPXMock): assert retrieved_orders2[0].cancelable == False +@pytest.mark.asyncio +async def test_place_order(httpx_mock: HTTPXMock): + mocked_token = mock_tokens() + token_store = LocalTokenStore() + if os.path.exists(Path(token_store.token_output_path)): + os.remove(token_store.token_output_path) # clean up before test + + opt_order1 = Order( + session=Session.NORMAL, + duration=Duration.GOOD_TILL_CANCEL, + orderType=OrderType.NET_DEBIT, + complexOrderStrategyType=ComplexOrderStrategyType.BUTTERFLY, + price=0.9, + orderLegCollection=[ + OrderLegCollection( + orderLegType=AssetType.OPTION, + instrument=AccountInstrument( + assetType=AssetType.OPTION, symbol="SPXW 240701C05530000" + ), + instruction=OrderLegInstruction.BUY_TO_OPEN, + positionEffect=PositionEffect.OPENING, + quantity=1, + ), + OrderLegCollection( + orderLegType=AssetType.OPTION, + instrument=AccountInstrument( + assetType=AssetType.OPTION, symbol="SPXW 240701C05540000" + ), + instruction=OrderLegInstruction.SELL_TO_OPEN, + positionEffect=PositionEffect.OPENING, + quantity=2, + ), + OrderLegCollection( + orderLegType=AssetType.OPTION, + instrument=AccountInstrument( + assetType=AssetType.OPTION, symbol="SPXW 240701C05550000" + ), + instruction=OrderLegInstruction.BUY_TO_OPEN, + positionEffect=PositionEffect.OPENING, + quantity=1, + ), + ], + ) + + order_id = 1000847830245 + token_store.save_tokens(mocked_token) + location_url = ( + f"https://api.schwabapi.com/trader/v1/accounts/HASHHERE/orders/{order_id}" + ) + headers = {"Location": location_url} + httpx_mock.add_response(headers=headers, status_code=201) + async with httpx.AsyncClient() as client: + cschwab_client = SchwabAsyncClient( + app_client_id="fake_id", + app_secret="fake_secret", + token_store=token_store, + tokens=mocked_token, + http_client=client, + ) + new_order_id = await cschwab_client.place_order_async( + account_number_hash=mock_account(), order=opt_order1 + ) + assert new_order_id == order_id + + with httpx.Client() as client2: + cschwab_client = SchwabClient( + app_client_id="fake_id", + app_secret="fake_secret", + token_store=token_store, + http_client=client2, + ) + + new_order_id2 = cschwab_client.place_order( + account_number_hash=mock_account(), + order=opt_order1, + ) + assert new_order_id2 == order_id + + @pytest.mark.asyncio async def test_get_order_by_id(httpx_mock: HTTPXMock): json_mock = get_mock_response()["filled_order"]