diff --git a/alpaca/broker/client.py b/alpaca/broker/client.py index b01c5c09..13e47c05 100644 --- a/alpaca/broker/client.py +++ b/alpaca/broker/client.py @@ -1,5 +1,5 @@ import base64 -from typing import Callable, Iterator, List, Optional, Union, Dict +from typing import Callable, Iterator, List, Optional, Union from uuid import UUID import sseclient @@ -7,18 +7,21 @@ from pydantic import TypeAdapter from requests import HTTPError, Response + from .enums import ACHRelationshipStatus from alpaca.broker.models import ( ACHRelationship, Account, Bank, - CIPInfo, TradeAccount, TradeDocument, Transfer, Order, BatchJournalResponse, Journal, + BaseActivity, + NonTradeActivity, + TradeActivity, ) from .requests import ( CreateJournalRequest, @@ -59,11 +62,6 @@ CorporateActionAnnouncement, AccountConfiguration as TradeAccountConfiguration, ) -from alpaca.trading.models import ( - BaseActivity, - NonTradeActivity, - TradeActivity, -) from alpaca.trading.requests import ( GetPortfolioHistoryRequest, ClosePositionRequest, diff --git a/alpaca/broker/models/__init__.py b/alpaca/broker/models/__init__.py index b02b2ec7..45b01be2 100644 --- a/alpaca/broker/models/__init__.py +++ b/alpaca/broker/models/__init__.py @@ -1,4 +1,5 @@ from .accounts import * +from .activities import * from .cip import * from .documents import * from .funding import * diff --git a/alpaca/broker/models/activities.py b/alpaca/broker/models/activities.py new file mode 100644 index 00000000..46fd1e76 --- /dev/null +++ b/alpaca/broker/models/activities.py @@ -0,0 +1,33 @@ +from uuid import UUID +from alpaca.trading.models import BaseActivity as TradingBaseActivity +from alpaca.trading.models import NonTradeActivity as BaseNonTradeActivity +from alpaca.trading.models import TradeActivity as BaseTradeActivity + + +class BaseActivity(TradingBaseActivity): + """ + Base model for activities that are retrieved through the Broker API. + + Attributes: + id (str): Unique ID of this Activity. Note that IDs for Activity instances are formatted like + `20220203000000000::045b3b8d-c566-4bef-b741-2bf598dd6ae7` the first part before the `::` is a date string + while the part after is a UUID + account_id (UUID): id of the Account this activity relates too + activity_type (ActivityType): What specific kind of Activity this was + """ + + account_id: UUID + + def __init__(self, *args, **data): + if "account_id" in data and type(data["account_id"]) == str: + data["account_id"] = UUID(data["account_id"]) + + super().__init__(*args, **data) + + +class NonTradeActivity(BaseNonTradeActivity, BaseActivity): + """NonTradeActivity for the Broker API.""" + + +class TradeActivity(BaseTradeActivity, BaseActivity): + """TradeActivity for the Broker API.""" diff --git a/alpaca/broker/requests.py b/alpaca/broker/requests.py index c8679dd9..14d3ee11 100644 --- a/alpaca/broker/requests.py +++ b/alpaca/broker/requests.py @@ -18,11 +18,9 @@ AccountEntities, BankAccountType, DocumentType, - EmploymentStatus, FeePaymentMethod, FundingSource, IdentifierType, - TaxIdType, TradeDocumentType, TransferDirection, TransferTiming, @@ -44,6 +42,7 @@ StopLimitOrderRequest as BaseStopLimitOrderRequest, TrailingStopOrderRequest as BaseTrailingStopOrderRequest, CancelOrderResponse as BaseCancelOrderResponse, + GetAccountActivitiesRequest as BaseGetAccountActivitiesRequest, ) @@ -260,7 +259,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) -class GetAccountActivitiesRequest(NonEmptyRequest): +class GetAccountActivitiesRequest(BaseGetAccountActivitiesRequest): """ Represents the filtering values you can specify when getting AccountActivities for an Account @@ -299,13 +298,6 @@ class GetAccountActivitiesRequest(NonEmptyRequest): """ account_id: Optional[Union[UUID, str]] = None - activity_types: Optional[List[ActivityType]] = None - date: Optional[datetime] = None - until: Optional[datetime] = None - after: Optional[datetime] = None - direction: Optional[Sort] = None - page_size: Optional[int] = None - page_token: Optional[Union[UUID, str]] = None def __init__(self, *args, **kwargs): if "account_id" in kwargs and type(kwargs["account_id"]) == str: @@ -313,22 +305,6 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - @model_validator(mode="before") - def root_validator(cls, values: dict) -> dict: - """Verify that certain conflicting params aren't set""" - - date_set = "date" in values and values["date"] is not None - after_set = "after" in values and values["after"] is not None - until_set = "until" in values and values["until"] is not None - - if date_set and after_set: - raise ValueError("Cannot set date and after at the same time") - - if date_set and until_set: - raise ValueError("Cannot set date and until at the same time") - - return values - # ############################## Documents ################################# # diff --git a/alpaca/common/rest.py b/alpaca/common/rest.py index adbc5660..66e7d79d 100644 --- a/alpaca/common/rest.py +++ b/alpaca/common/rest.py @@ -1,7 +1,7 @@ import time import base64 from abc import ABC -from typing import Any, List, Optional, Type, Union, Tuple, Iterator +from typing import List, Optional, Type, Union, Iterator from pydantic import BaseModel from requests import Session diff --git a/alpaca/trading/client.py b/alpaca/trading/client.py index 47a69e72..056de8b3 100644 --- a/alpaca/trading/client.py +++ b/alpaca/trading/client.py @@ -5,8 +5,8 @@ from alpaca.common import RawData from alpaca.common.utils import validate_uuid_id_param, validate_symbol_or_asset_id from alpaca.common.rest import RESTClient -from typing import Optional, List, Union -from alpaca.common.enums import BaseURL +from typing import Callable, Iterator, Optional, List, Union +from alpaca.common.enums import BaseURL, PaginationType from alpaca.trading.requests import ( GetCalendarRequest, @@ -20,13 +20,17 @@ CreateWatchlistRequest, UpdateWatchlistRequest, GetCorporateAnnouncementsRequest, + GetAccountActivitiesRequest, ) from alpaca.trading.models import ( + BaseActivity, + NonTradeActivity, Order, Position, ClosePositionResponse, Asset, + TradeActivity, Watchlist, Clock, Calendar, @@ -35,6 +39,14 @@ AccountConfiguration, ) +from alpaca.common.types import HTTPResult + +from alpaca.common.constants import ACCOUNT_ACTIVITIES_DEFAULT_PAGE_SIZE + +from alpaca.common.exceptions import APIError + +from alpaca.trading.enums import ActivityType + class TradingClient(RESTClient): """ @@ -455,6 +467,150 @@ def set_account_configurations( return AccountConfiguration(**json.loads(response)) + # ############################## ACCOUNT ACTIVITIES ######################## # + + def get_account_activities( + self, + activity_filter: GetAccountActivitiesRequest, + max_items_limit: Optional[int] = None, + handle_pagination: Optional[PaginationType] = None, + ) -> Union[List[BaseActivity], Iterator[List[BaseActivity]]]: + """ + Gets a list of Account activities, with various filtering options. Please see the documentation for + GetAccountActivitiesRequest for more information as to what filters are available. + + The return type of this function is List[BaseActivity] however the list will contain concrete instances of one + of the child classes of BaseActivity, either TradeActivity or NonTradeActivity. It can be a mixed list depending + on what filtering criteria you pass through `activity_filter` + + + Args: + activity_filter (GetAccountActivitiesRequest): The various filtering fields you can specify to restrict + results + max_items_limit (Optional[int]): A maximum number of items to return over all for when handle_pagination is + of type `PaginationType.FULL`. Ignored otherwise. + handle_pagination (Optional[PaginationType]): What kind of pagination you want. If None then defaults to + `PaginationType.FULL` + + Returns: + Union[List[BaseActivity], Iterator[List[BaseActivity]]]: Either a list or an Iterator of lists of + BaseActivity child classes + """ + handle_pagination = TradingClient._validate_pagination( + max_items_limit, handle_pagination + ) + + # otherwise, user wants pagination so we grab an interator + iterator = self._get_account_activities_iterator( + activity_filter=activity_filter, + max_items_limit=max_items_limit, + mapping=lambda raw_activities: [ + TradingClient._parse_activity(activity) for activity in raw_activities + ], + ) + + return TradingClient._return_paginated_result(iterator, handle_pagination) + + def _get_account_activities_iterator( + self, + activity_filter: GetAccountActivitiesRequest, + mapping: Callable[[HTTPResult], List[BaseActivity]], + max_items_limit: Optional[int] = None, + ) -> Iterator[List[BaseActivity]]: + """ + Private method for handling the iterator parts of get_account_activities + """ + + # we need to track total items retrieved + total_items = 0 + request_fields = activity_filter.to_request_fields() + + while True: + """ + we have a couple cases to handle here: + - max limit isn't set, so just handle normally + - max is set, and page_size isn't + - date isn't set. So we'll fall back to the default page size + - date is set, in this case the api is allowed to not page and return all results. Need to make + sure only take the we allow for making still a single request here but only taking the items we + need, in case user wanted only 1 request to happen. + - max is set, and page_size is also set. Keep track of total_items and run a min check every page to + see if we need to take less than the page_size items + """ + + if max_items_limit is not None: + page_size = ( + activity_filter.page_size + if activity_filter.page_size is not None + else ACCOUNT_ACTIVITIES_DEFAULT_PAGE_SIZE + ) + + normalized_page_size = min( + int(max_items_limit) - total_items, page_size + ) + + request_fields["page_size"] = normalized_page_size + + result = self.get("/account/activities", request_fields) + + # the api returns [] when it's done + + if not isinstance(result, List) or len(result) == 0: + break + + num_items_returned = len(result) + + # need to handle the case where the api won't page and returns all results, ie `date` is set + if ( + max_items_limit is not None + and num_items_returned + total_items > max_items_limit + ): + result = result[: (max_items_limit - total_items)] + + total_items += max_items_limit - total_items + else: + total_items += num_items_returned + + yield mapping(result) + + if max_items_limit is not None and total_items >= max_items_limit: + break + + # ok we made it to the end, we need to ask for the next page of results + last_result = result[-1] + + if "id" not in last_result: + raise APIError( + "AccountActivity didn't contain an `id` field to use for paginating results" + ) + + # set the pake token to the id of the last activity so we can get the next page + request_fields["page_token"] = last_result["id"] + + @staticmethod + def _parse_activity(data: dict) -> Union[TradeActivity, NonTradeActivity]: + """ + We cannot just use TypeAdapter for Activity types since we need to know what child instance to cast it into. + + So this method does just that. + + Args: + data (dict): a dict of raw data to attempt to convert into an Activity instance + + Raises: + ValueError: Will raise a ValueError if `data` doesn't contain an `activity_type` field to compare + """ + + if "activity_type" not in data or data["activity_type"] is None: + raise ValueError( + "Failed parsing raw activity data, `activity_type` is not present in fields" + ) + + if ActivityType.is_str_trade_activity(data["activity_type"]): + return TypeAdapter(TradeActivity).validate_python(data) + else: + return TypeAdapter(NonTradeActivity).validate_python(data) + # ############################## WATCHLIST ################################# # def get_watchlists( diff --git a/alpaca/trading/models.py b/alpaca/trading/models.py index 34b93033..e287f274 100644 --- a/alpaca/trading/models.py +++ b/alpaca/trading/models.py @@ -376,20 +376,12 @@ class BaseActivity(BaseModel): id (str): Unique ID of this Activity. Note that IDs for Activity instances are formatted like `20220203000000000::045b3b8d-c566-4bef-b741-2bf598dd6ae7` the first part before the `::` is a date string while the part after is a UUID - account_id (UUID): id of the Account this activity relates too activity_type (ActivityType): What specific kind of Activity this was """ id: str - account_id: UUID activity_type: ActivityType - def __init__(self, *args, **data): - if "account_id" in data and type(data["account_id"]) == str: - data["account_id"] = UUID(data["account_id"]) - - super().__init__(*args, **data) - class NonTradeActivity(BaseActivity): """ diff --git a/alpaca/trading/requests.py b/alpaca/trading/requests.py index 4f8900fa..7786b693 100644 --- a/alpaca/trading/requests.py +++ b/alpaca/trading/requests.py @@ -1,5 +1,5 @@ from datetime import date, datetime, timedelta -from typing import Optional, Any, List +from typing import Optional, Any, List, Union from uuid import UUID import pandas as pd @@ -9,6 +9,7 @@ from alpaca.common.enums import Sort from alpaca.common.models import ValidateBaseModel as BaseModel from alpaca.trading.enums import ( + ActivityType, OrderType, AssetStatus, AssetClass, @@ -466,3 +467,66 @@ def root_validator(cls, values: dict) -> dict: raise ValueError("The date range is limited to 90 days.") return values + + +class GetAccountActivitiesRequest(NonEmptyRequest): + """ + Represents the filtering values you can specify when getting AccountActivities for an Account + + **Notes on pagination and the `page_size` and `page_token` fields**. + + The BrokerClient::get_account_activities function by default will automatically handle the pagination of results + for you to get all results at once. However, if you're requesting a very large amount of results this can use a + large amount of memory and time to gather all the results. If you instead want to handle + pagination yourself `page_size` and `page_token` are how you would handle this. + + Say you put in a request with `page_size` set to 4, you'll only get 4 results back to get + the next "page" of results you would set `page_token` to be the `id` field of the last Activity returned in the + result set. + + This gets more indepth if you start specifying the `sort` field as well. If specified with a direction of Sort.DESC, + for example, the results will end before the activity with the specified ID. However, specified with a direction of + Sort.ASC, results will begin with the activity immediately after the one specified. + + Also, to note if `date` is not specified, the default and maximum `page_size` value is 100. If `date` is specified, + the default behavior is to return all results, and there is no maximum page size; page size is still supported in + this state though. + + Please see https://alpaca.markets/docs/api-references/broker-api/accounts/account-activities/#retrieving-account-activities + for more information + + Attributes: + account_id (Optional[Union[UUID, str]]): Specifies to filter to only activities for this Account + activity_types (Optional[List[ActivityType]]): A list of ActivityType's to filter results down to + date (Optional[datetime]): Filter to Activities only on this date. + until (Optional[datetime]): Filter to Activities before this date. Cannot be used if `date` is also specified. + after (Optional[datetime]): Filter to Activities after this date. Cannot be used if `date` is also specified. + direction (Optional[Sort]): Which direction to sort results in. Defaults to Sort.DESC + page_size (Optional[int]): The maximum number of entries to return in the response + page_token (Optional[Union[UUID, str]]): If you're not using the built-in pagination this field is what you + would use to mark the end of the results of your last page. + """ + + activity_types: Optional[List[ActivityType]] = None + date: Optional[datetime] = None + until: Optional[datetime] = None + after: Optional[datetime] = None + direction: Optional[Sort] = None + page_size: Optional[int] = None + page_token: Optional[Union[UUID, str]] = None + + @model_validator(mode="before") + def root_validator(cls, values: dict) -> dict: + """Verify that certain conflicting params aren't set""" + + date_set = "date" in values and values["date"] is not None + after_set = "after" in values and values["after"] is not None + until_set = "until" in values and values["until"] is not None + + if date_set and after_set: + raise ValueError("Cannot set date and after at the same time") + + if date_set and until_set: + raise ValueError("Cannot set date and until at the same time") + + return values diff --git a/tests/broker/broker_client/test_account_activities_routes.py b/tests/broker/broker_client/test_account_activities_routes.py index 58b9b81e..dd12e69e 100644 --- a/tests/broker/broker_client/test_account_activities_routes.py +++ b/tests/broker/broker_client/test_account_activities_routes.py @@ -9,7 +9,7 @@ GetAccountActivitiesRequest, ) from alpaca.common.enums import BaseURL -from alpaca.trading.models import ( +from alpaca.broker.models import ( BaseActivity, NonTradeActivity, TradeActivity, diff --git a/tests/trading/trading_client/test_account_activities_routes.py b/tests/trading/trading_client/test_account_activities_routes.py new file mode 100644 index 00000000..7e681f4b --- /dev/null +++ b/tests/trading/trading_client/test_account_activities_routes.py @@ -0,0 +1,332 @@ +from datetime import datetime +from typing import Iterator, List + +import pytest +from requests_mock import Mocker + +from alpaca.trading.client import TradingClient, PaginationType +from alpaca.trading.requests import ( + GetAccountActivitiesRequest, +) +from alpaca.common.enums import BaseURL +from alpaca.trading.models import ( + BaseActivity, + NonTradeActivity, + TradeActivity, +) + + +def setup_reqmock_for_paginated_account_activities_response(reqmock: Mocker): + resp_one = """ + [ + { + "id": "20220419000000000::fd84741b-59c5-4ddd-a303-69f70eb7753f", + "activity_type": "CSD", + "date": "2022-04-19", + "net_amount": "33324.35", + "description": "", + "status": "executed" + }, + { + "id": "20220419000000000::fb876acb-76b0-405c-8c7f-96a1c171ec5c", + "activity_type": "CSD", + "date": "2022-04-19", + "net_amount": "29161.91", + "description": "", + "status": "executed" + }, + { + "id": "20220304095318500::092cd749-b783-49cb-a36e-4d8666be201f", + "activity_type": "FILL", + "transaction_time": "2022-03-04T14:53:18.500245Z", + "type": "fill", + "price": "2630.95", + "qty": "9", + "side": "buy", + "symbol": "GOOGL", + "leaves_qty": "0", + "order_id": "b677e464-c2d0-4fdd-a4b1-8830b386aa50", + "cum_qty": "10.177047834", + "order_status": "filled" + }, + { + "id": "20220419000000000::f77b60bf-ea39-4551-a3d6-000548e6f11c", + "activity_type": "CSD", + "date": "2022-04-19", + "net_amount": "45850.47", + "description": "", + "status": "executed" + } + ] + """ + resp_two = """ + [ + { + "id": "20220419000000000::ed22fc4d-897c-474b-876a-b492d40f83d2", + "activity_type": "CSD", + "date": "2022-04-19", + "net_amount": "43864.18", + "description": "", + "status": "executed" + }, + { + "id": "20220419000000000::ec75b06d-1d29-4d1e-9143-c7e59aa842bc", + "activity_type": "CSD", + "date": "2022-04-19", + "net_amount": "32155.97", + "description": "", + "status": "executed" + }, + { + "id": "20220419000000000::ec624f60-ca70-42d6-9086-f47a1eeebeb7", + "activity_type": "CSD", + "date": "2022-04-19", + "net_amount": "20979.69", + "description": "", + "status": "executed" + }, + { + "activity_type": "DIV", + "id": "20190801011955195::5f596936-6f23-4cef-bdf1-3806aae57dbf", + "date": "2019-08-01", + "net_amount": "1.02", + "symbol": "T", + "description": "", + "qty": "2", + "per_share_amount": "0.51" + } + ] + """ + + reqmock.get( + BaseURL.TRADING_PAPER.value + "/v2/account/activities", + [{"text": resp_one}, {"text": resp_two}, {"text": """[]"""}], + ) + + +def test_get_activities_for_account_default_asserts( + reqmock, trading_client: TradingClient +): + setup_reqmock_for_paginated_account_activities_response(reqmock) + + result = trading_client.get_account_activities(GetAccountActivitiesRequest()) + + assert reqmock.call_count == 3 + assert isinstance(result, List) + assert len(result) == 8 + assert isinstance(result[0], NonTradeActivity) + assert isinstance(result[2], TradeActivity) + + # verify we asked for the correct ids when paginating + assert reqmock.request_history[1].qs == { + "page_token": ["20220419000000000::f77b60bf-ea39-4551-a3d6-000548e6f11c"] + } + + +def test_get_activities_for_account_full_pagination( + reqmock, trading_client: TradingClient +): + setup_reqmock_for_paginated_account_activities_response(reqmock) + + result = trading_client.get_account_activities( + GetAccountActivitiesRequest(), handle_pagination=PaginationType.FULL + ) + + assert reqmock.call_count == 3 + assert isinstance(result, List) + assert len(result) == 8 + assert isinstance(result[0], NonTradeActivity) + assert isinstance(result[2], TradeActivity) + + +def test_get_activities_for_account_max_items_and_single_request_date( + reqmock, + trading_client: TradingClient, +): + """ + The api when `date` is specified is allowed to drop the pagination defaults and return all results at once. + This test is to ensure in this case if there is a max items requested that we still only request + that max items amount. + """ + + # Note we purposly have this returning more than requested, the api currently respects paging even in this state + # but we should still be able to handle the case where it doesn't, so we don't go over max items + reqmock.get( + BaseURL.TRADING_PAPER.value + "/v2/account/activities", + text=""" + [ + { + "id": "20220304135420903::047e252a-a8a3-4e35-84e2-29814cbf5057", + "activity_type": "FILL", + "transaction_time": "2022-03-04T18:54:20.903569Z", + "type": "partial_fill", + "price": "2907.15", + "qty": "1.792161878", + "side": "buy", + "symbol": "AMZN", + "leaves_qty": "1", + "order_id": "cddf433b-1a41-497d-ae31-50b1fee56fff", + "cum_qty": "1.792161878", + "order_status": "partially_filled" + }, + { + "id": "20220304135420898::2b9e8979-48b4-4b70-9ba0-008210b76ebf", + "activity_type": "FILL", + "transaction_time": "2022-03-04T18:54:20.89822Z", + "type": "fill", + "price": "2907.15", + "qty": "1", + "side": "buy", + "symbol": "AMZN", + "leaves_qty": "0", + "order_id": "cddf433b-1a41-497d-ae31-50b1fee56fff", + "cum_qty": "2.792161878", + "order_status": "filled" + }, + { + "id": "20220304123922801::3b8a937c-b1d9-4ebe-ae94-5e0b52c3f350", + "activity_type": "FILL", + "transaction_time": "2022-03-04T17:39:22.801228Z", + "type": "fill", + "price": "2644.84", + "qty": "0.058773239", + "side": "sell", + "symbol": "GOOGL", + "leaves_qty": "0", + "order_id": "642695e3-def7-4637-9525-2e7f698ebfc7", + "cum_qty": "0.058773239", + "order_status": "filled" + }, + { + "id": "20220304123922310::b53b6d71-a644-4be1-9f88-39d1c8d29831", + "activity_type": "FILL", + "transaction_time": "2022-03-04T17:39:22.310917Z", + "type": "partial_fill", + "price": "837.45", + "qty": "1.998065556", + "side": "sell", + "symbol": "TSLA", + "leaves_qty": "4", + "order_id": "5f4a07dc-6503-4cbf-902a-8c6608401d97", + "cum_qty": "1.998065556", + "order_status": "partially_filled" + }, + { + "id": "20220304123922305::bc84b8a8-8758-42aa-be3b-618d097c2867", + "activity_type": "FILL", + "transaction_time": "2022-03-04T17:39:22.305629Z", + "type": "fill", + "price": "837.45", + "qty": "4", + "side": "sell", + "symbol": "TSLA", + "leaves_qty": "0", + "order_id": "5f4a07dc-6503-4cbf-902a-8c6608401d97", + "cum_qty": "5.998065556", + "order_status": "filled" + } + ] + """, + ) + + max_limit = 2 + date_str = "2022-03-04" + + result = trading_client.get_account_activities( + GetAccountActivitiesRequest(date=datetime.strptime(date_str, "%Y-%m-%d")), + handle_pagination=PaginationType.FULL, + max_items_limit=max_limit, + ) + + assert reqmock.call_count == 1 + assert isinstance(result, List) + assert len(result) == max_limit + + request = reqmock.request_history[0] + assert "date" in request.qs and request.qs["date"] == [f"{date_str}t00:00:00z"] + assert "page_size" in request.qs and request.qs["page_size"] == ["2"] + + +def test_get_activities_for_account_full_pagination_and_max_items( + reqmock, + trading_client: TradingClient, +): + # Note in this test we'll still have the api return too many results in the response just to validate that + # we respect max limit regardless of what the api does + setup_reqmock_for_paginated_account_activities_response(reqmock) + + max_limit = 5 + + result = trading_client.get_account_activities( + GetAccountActivitiesRequest(), + handle_pagination=PaginationType.FULL, + max_items_limit=max_limit, + ) + + assert reqmock.call_count == 2 + assert isinstance(result, List) + assert len(result) == max_limit + + # First limit is irrelevant since we hardcode returning 4 anyway, but second request needs to only request 1 item + second_request = reqmock.request_history[1] + assert "page_size" in second_request.qs and second_request.qs["page_size"] == ["1"] + + +def test_get_activities_for_account_none_pagination( + reqmock, trading_client: TradingClient +): + setup_reqmock_for_paginated_account_activities_response(reqmock) + + result = trading_client.get_account_activities( + GetAccountActivitiesRequest(), handle_pagination=PaginationType.NONE + ) + + assert reqmock.call_count == 1 + assert isinstance(result, List) + assert len(result) == 4 + assert isinstance(result[0], BaseActivity) + + +def test_get_account_activities_iterator_pagination( + reqmock, trading_client: TradingClient +): + setup_reqmock_for_paginated_account_activities_response(reqmock) + + generator = trading_client.get_account_activities( + GetAccountActivitiesRequest(), handle_pagination=PaginationType.ITERATOR + ) + + assert isinstance(generator, Iterator) + + # When asking for an iterator we should not have made any requests yet + assert not reqmock.called + + results = next(generator) + + assert isinstance(results, List) + assert len(results) == 4 + assert isinstance(results[0], BaseActivity) + assert reqmock.called_once + + results = next(generator) + assert isinstance(results, List) + assert len(results) == 4 + + # generator should now be empty + results = next(generator, None) + assert reqmock.call_count == 3 + + assert results is None + + +def test_get_account_activities_validates_max_items(reqmock, client: TradingClient): + with pytest.raises(ValueError) as e: + client.get_account_activities( + GetAccountActivitiesRequest(), + max_items_limit=45, + handle_pagination=PaginationType.ITERATOR, + ) + + assert "max_items_limit can only be specified for PaginationType.FULL" in str( + e.value + )