diff --git a/tastytrade/account.py b/tastytrade/account.py index e81c931..f3d3f32 100644 --- a/tastytrade/account.py +++ b/tastytrade/account.py @@ -832,11 +832,11 @@ def get_margin_requirements(self, session: Session) -> MarginReport: def get_live_orders(self, session: Session) -> List[PlacedOrder]: """ - Get all live orders for the account. + Get orders placed today for the account. :param session: the session to use for the request. - :return: a list of :class:`Order` objects. + :return: a list of :class:`PlacedOrder` objects. """ response = requests.get( f'{session.base_url}/accounts/{self.account_number}/orders/live', @@ -848,6 +848,28 @@ def get_live_orders(self, session: Session) -> List[PlacedOrder]: return [PlacedOrder(**entry) for entry in data] + def get_live_complex_orders( + self, + session: Session + ) -> List[PlacedComplexOrder]: + """ + Get complex orders placed today for the account. + + :param session: the session to use for the request. + + :return: a list of :class:`PlacedComplexOrder` objects. + """ + response = requests.get( + (f'{session.base_url}/accounts/{self.account_number}' + f'/complex-orders/live'), + headers=session.headers + ) + validate_response(response) + + data = response.json()['data']['items'] + + return [PlacedComplexOrder(**entry) for entry in data] + def get_complex_order( self, session: Session, @@ -998,6 +1020,57 @@ def get_order_history( return [PlacedOrder(**entry) for entry in results] + def get_complex_order_history( + self, + session: Session, + per_page: int = 50, + page_offset: Optional[int] = None + ) -> List[PlacedComplexOrder]: + """ + Get order history of the account. + + :param session: the session to use for the request. + :param per_page: the number of results to return per page. + :param page_offset: + provide a specific page to get; if not provided, get all pages + + :return: + a list of Tastytrade 'PlacedComplexOrder' objects in JSON format. + """ + # if a specific page is provided, we just get that page; + # otherwise, we loop through all pages + paginate = False + if page_offset is None: + page_offset = 0 + paginate = True + params: Dict[str, Any] = { + 'per-page': per_page, + 'page-offset': page_offset + } + + # loop through pages and get all transactions + results = [] + while True: + response = requests.get( + (f'{session.base_url}/accounts/{self.account_number}' + f'/complex-orders'), + headers=session.headers, + params={k: v for k, v in params.items() if v is not None} + ) + validate_response(response) + + json = response.json() + results.extend(json['data']['items']) + + pagination = json['pagination'] + if pagination['page-offset'] >= pagination['total-pages'] - 1: + break + if not paginate: + break + params['page-offset'] += 1 # type: ignore + + return [PlacedComplexOrder(**entry) for entry in results] + def place_order( self, session: Session, diff --git a/tastytrade/session.py b/tastytrade/session.py index c7de99a..587b4f8 100644 --- a/tastytrade/session.py +++ b/tastytrade/session.py @@ -148,6 +148,8 @@ class ProductionSession(Session): :param two_factor_authentication: if two factor authentication is enabled, this is the code sent to the user's device + :param dxfeed_tos_compliant: + whether to use the dxfeed TOS-compliant API endpoint for the streamer """ def __init__( self, @@ -155,7 +157,8 @@ def __init__( password: Optional[str] = None, remember_me: bool = False, remember_token: Optional[str] = None, - two_factor_authentication: Optional[str] = None + two_factor_authentication: Optional[str] = None, + dxfeed_tos_compliant: bool = False ): body = { 'login': login, @@ -203,8 +206,11 @@ def __init__( self.validate() # Pull streamer tokens and urls + url = ('api-quote-tokens' + if dxfeed_tos_compliant + else 'quote-streamer-tokens') response = requests.get( - f'{self.base_url}/quote-streamer-tokens', + f'{self.base_url}/{url}', headers=self.headers ) validate_response(response) diff --git a/tests/test_account.py b/tests/test_account.py index 5482add..8ddfb25 100644 --- a/tests/test_account.py +++ b/tests/test_account.py @@ -15,7 +15,7 @@ def account(session): return Account.get_accounts(session)[0] -@pytest.fixture(scope='session') +@pytest.fixture def cert_session(get_cert_credentials): usr, pwd = get_cert_credentials session = CertificationSession(usr, pwd) @@ -23,9 +23,8 @@ def cert_session(get_cert_credentials): session.destroy() -@pytest.fixture(scope='session') -def cert_account(cert_session): - return Account.get_account(cert_session, '5WZ97189') +def test_cert_accounts(cert_session): + assert Account.get_accounts(cert_session) != [] def test_get_account(session, account): @@ -75,14 +74,14 @@ def test_get_effective_margin_requirements(session, account): @pytest.fixture(scope='session') def new_order(session): - symbol = Equity.get_equity(session, 'SPY') + symbol = Equity.get_equity(session, 'NVDA') leg = symbol.build_leg(Decimal(1), OrderAction.BUY_TO_OPEN) return NewOrder( time_in_force=OrderTimeInForce.DAY, order_type=OrderType.LIMIT, legs=[leg], - price=Decimal(42), # if this fills the US has crumbled + price=Decimal(10), # if this fills the US has crumbled price_effect=PriceEffect.DEBIT ) @@ -104,7 +103,7 @@ def test_get_order(session, account, placed_order): def test_replace_and_delete_order(session, account, new_order, placed_order): modified_order = new_order.model_copy() - modified_order.price = Decimal(40) + modified_order.price = Decimal(11) replaced = account.replace_order(session, placed_order.id, modified_order) sleep(3) account.delete_order(session, replaced.id) @@ -114,25 +113,17 @@ def test_get_order_history(session, account): account.get_order_history(session, page_offset=0) +def test_get_complex_order_history(session, account): + account.get_complex_order_history(session, page_offset=0) + + def test_get_live_orders(session, account): account.get_live_orders(session) -def test_place_oco_order(cert_session, cert_account): - session = cert_session - account = cert_account - # first, buy share of SPY to set up the OCO order - symbol = Equity.get_equity(session, 'SPY') - opening = symbol.build_leg(Decimal(1), OrderAction.BUY_TO_OPEN) - resp1 = account.place_order(session, NewOrder( - time_in_force=OrderTimeInForce.DAY, - order_type=OrderType.LIMIT, - legs=[opening], - price=Decimal('2.5'), # should fill immediately for cert account - price_effect=PriceEffect.DEBIT - ), dry_run=False) - assert resp1.order.status != OrderStatus.REJECTED - +def test_place_oco_order(session, account): + # account must have a share of NVDA for this to work + symbol = Equity.get_equity(session, 'NVDA') closing = symbol.build_leg(Decimal(1), OrderAction.SELL_TO_CLOSE) oco = NewComplexOrder( orders=[ @@ -159,9 +150,7 @@ def test_place_oco_order(cert_session, cert_account): account.delete_complex_order(session, resp2.complex_order.id) -def test_place_otoco_order(cert_session, cert_account): - session = cert_session - account = cert_account +def test_place_otoco_order(session, account): symbol = Equity.get_equity(session, 'AAPL') opening = symbol.build_leg(Decimal(1), OrderAction.BUY_TO_OPEN) closing = symbol.build_leg(Decimal(1), OrderAction.SELL_TO_CLOSE) @@ -170,7 +159,7 @@ def test_place_otoco_order(cert_session, cert_account): time_in_force=OrderTimeInForce.DAY, order_type=OrderType.LIMIT, legs=[opening], - price=Decimal('250'), # won't fill + price=Decimal('100'), # won't fill price_effect=PriceEffect.DEBIT ), orders=[ @@ -178,7 +167,7 @@ def test_place_otoco_order(cert_session, cert_account): time_in_force=OrderTimeInForce.GTC, order_type=OrderType.LIMIT, legs=[closing], - price=Decimal('2500'), # won't fill + price=Decimal('400'), # won't fill price_effect=PriceEffect.CREDIT ), NewOrder( @@ -193,3 +182,8 @@ def test_place_otoco_order(cert_session, cert_account): resp = account.place_complex_order(session, otoco, dry_run=False) sleep(3) account.delete_complex_order(session, resp.complex_order.id) + + +def test_get_live_complex_orders(session, account): + orders = account.get_live_complex_orders(session) + assert orders != []