alpaca-py-nopandas 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. alpaca/__init__.py +2 -0
  2. alpaca/broker/__init__.py +8 -0
  3. alpaca/broker/client.py +2360 -0
  4. alpaca/broker/enums.py +528 -0
  5. alpaca/broker/models/__init__.py +7 -0
  6. alpaca/broker/models/accounts.py +347 -0
  7. alpaca/broker/models/cip.py +265 -0
  8. alpaca/broker/models/documents.py +159 -0
  9. alpaca/broker/models/funding.py +114 -0
  10. alpaca/broker/models/journals.py +71 -0
  11. alpaca/broker/models/rebalancing.py +80 -0
  12. alpaca/broker/models/trading.py +13 -0
  13. alpaca/broker/requests.py +1135 -0
  14. alpaca/common/__init__.py +6 -0
  15. alpaca/common/constants.py +13 -0
  16. alpaca/common/enums.py +64 -0
  17. alpaca/common/exceptions.py +47 -0
  18. alpaca/common/models.py +21 -0
  19. alpaca/common/requests.py +82 -0
  20. alpaca/common/rest.py +438 -0
  21. alpaca/common/types.py +7 -0
  22. alpaca/common/utils.py +89 -0
  23. alpaca/data/__init__.py +5 -0
  24. alpaca/data/enums.py +184 -0
  25. alpaca/data/historical/__init__.py +13 -0
  26. alpaca/data/historical/corporate_actions.py +76 -0
  27. alpaca/data/historical/crypto.py +299 -0
  28. alpaca/data/historical/news.py +63 -0
  29. alpaca/data/historical/option.py +230 -0
  30. alpaca/data/historical/screener.py +72 -0
  31. alpaca/data/historical/stock.py +226 -0
  32. alpaca/data/historical/utils.py +30 -0
  33. alpaca/data/live/__init__.py +11 -0
  34. alpaca/data/live/crypto.py +168 -0
  35. alpaca/data/live/news.py +62 -0
  36. alpaca/data/live/option.py +88 -0
  37. alpaca/data/live/stock.py +199 -0
  38. alpaca/data/live/websocket.py +390 -0
  39. alpaca/data/mappings.py +84 -0
  40. alpaca/data/models/__init__.py +7 -0
  41. alpaca/data/models/bars.py +83 -0
  42. alpaca/data/models/base.py +45 -0
  43. alpaca/data/models/corporate_actions.py +309 -0
  44. alpaca/data/models/news.py +90 -0
  45. alpaca/data/models/orderbooks.py +59 -0
  46. alpaca/data/models/quotes.py +78 -0
  47. alpaca/data/models/screener.py +68 -0
  48. alpaca/data/models/snapshots.py +132 -0
  49. alpaca/data/models/trades.py +204 -0
  50. alpaca/data/requests.py +580 -0
  51. alpaca/data/timeframe.py +148 -0
  52. alpaca/py.typed +0 -0
  53. alpaca/trading/__init__.py +5 -0
  54. alpaca/trading/client.py +784 -0
  55. alpaca/trading/enums.py +412 -0
  56. alpaca/trading/models.py +697 -0
  57. alpaca/trading/requests.py +604 -0
  58. alpaca/trading/stream.py +225 -0
  59. alpaca_py_nopandas-0.1.0.dist-info/LICENSE +201 -0
  60. alpaca_py_nopandas-0.1.0.dist-info/METADATA +299 -0
  61. alpaca_py_nopandas-0.1.0.dist-info/RECORD +62 -0
  62. alpaca_py_nopandas-0.1.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,2360 @@
1
+ import base64
2
+ import warnings
3
+ from typing import Any, Callable, Dict, Iterator, List, Optional, Type, Union
4
+ from uuid import UUID
5
+
6
+ import sseclient
7
+ from pydantic import TypeAdapter
8
+ from requests import HTTPError, Response
9
+
10
+ from alpaca.broker.enums import ACHRelationshipStatus
11
+ from alpaca.broker.models import (
12
+ Account,
13
+ ACHRelationship,
14
+ Bank,
15
+ BaseModel,
16
+ BatchJournalResponse,
17
+ Journal,
18
+ Order,
19
+ Portfolio,
20
+ RebalancingRun,
21
+ Subscription,
22
+ TradeAccount,
23
+ TradeDocument,
24
+ Transfer,
25
+ )
26
+ from alpaca.broker.requests import (
27
+ CreateAccountRequest,
28
+ CreateACHRelationshipRequest,
29
+ CreateACHTransferRequest,
30
+ CreateBankRequest,
31
+ CreateBankTransferRequest,
32
+ CreateBatchJournalRequest,
33
+ CreateJournalRequest,
34
+ CreateOptionExerciseRequest,
35
+ CreatePlaidRelationshipRequest,
36
+ CreatePortfolioRequest,
37
+ CreateReverseBatchJournalRequest,
38
+ CreateRunRequest,
39
+ CreateSubscriptionRequest,
40
+ GetAccountActivitiesRequest,
41
+ GetEventsRequest,
42
+ GetJournalsRequest,
43
+ GetPortfoliosRequest,
44
+ GetRunsRequest,
45
+ GetSubscriptionsRequest,
46
+ GetTradeDocumentsRequest,
47
+ GetTransfersRequest,
48
+ ListAccountsRequest,
49
+ OrderRequest,
50
+ UpdateAccountRequest,
51
+ UpdatePortfolioRequest,
52
+ UploadDocumentRequest,
53
+ )
54
+ from alpaca.common import RawData
55
+ from alpaca.common.constants import (
56
+ ACCOUNT_ACTIVITIES_DEFAULT_PAGE_SIZE,
57
+ BROKER_DOCUMENT_UPLOAD_LIMIT,
58
+ )
59
+ from alpaca.common.enums import BaseURL, PaginationType
60
+ from alpaca.common.exceptions import APIError
61
+ from alpaca.common.rest import HTTPResult, RESTClient
62
+ from alpaca.common.utils import (
63
+ validate_symbol_or_asset_id,
64
+ validate_symbol_or_contract_id,
65
+ validate_uuid_id_param,
66
+ )
67
+ from alpaca.trading.enums import ActivityType
68
+ from alpaca.trading.models import AccountConfiguration as TradeAccountConfiguration
69
+ from alpaca.trading.models import (
70
+ AllAccountsPositions,
71
+ Asset,
72
+ BaseActivity,
73
+ Calendar,
74
+ Clock,
75
+ ClosePositionResponse,
76
+ CorporateActionAnnouncement,
77
+ NonTradeActivity,
78
+ PortfolioHistory,
79
+ Position,
80
+ TradeActivity,
81
+ Watchlist,
82
+ )
83
+ from alpaca.trading.requests import (
84
+ CancelOrderResponse,
85
+ ClosePositionRequest,
86
+ CreateWatchlistRequest,
87
+ GetAssetsRequest,
88
+ GetCalendarRequest,
89
+ GetCorporateAnnouncementsRequest,
90
+ GetOrderByIdRequest,
91
+ GetOrdersRequest,
92
+ GetPortfolioHistoryRequest,
93
+ ReplaceOrderRequest,
94
+ UpdateWatchlistRequest,
95
+ )
96
+
97
+ from ..common import RawData
98
+ from ..common.rest import HTTPResult, RESTClient
99
+ from .enums import ACHRelationshipStatus
100
+ from .requests import (
101
+ CreateAccountRequest,
102
+ CreateACHRelationshipRequest,
103
+ CreateACHTransferRequest,
104
+ CreateBankRequest,
105
+ CreateBankTransferRequest,
106
+ CreateBatchJournalRequest,
107
+ CreateJournalRequest,
108
+ CreatePlaidRelationshipRequest,
109
+ CreateReverseBatchJournalRequest,
110
+ GetAccountActivitiesRequest,
111
+ GetEventsRequest,
112
+ GetJournalsRequest,
113
+ GetTradeDocumentsRequest,
114
+ GetTransfersRequest,
115
+ ListAccountsRequest,
116
+ OrderRequest,
117
+ UpdateAccountRequest,
118
+ UploadDocumentRequest,
119
+ UploadW8BenDocumentRequest,
120
+ )
121
+
122
+
123
+ class BrokerClient(RESTClient):
124
+ """
125
+ Client for accessing Broker API services
126
+
127
+ **Note on the `handle_pagination` param you'll see across these methods**
128
+
129
+ By default, these methods will attempt to handle the fact that the API paginates results for the specific endpoint
130
+ for you by returning it all as one List.
131
+
132
+ However, that could:
133
+
134
+ 1. Take a long time if there are many results to paginate or if you request a small page size and have moderate
135
+ network latency
136
+ 2. Use up a large amount of memory to build all the results at once
137
+
138
+ So for those cases where a single list all at once would be prohibitive you can specify what kind of pagination you
139
+ want with the `handle_pagination` parameter. Please see the PaginationType enum for an explanation as to what the
140
+ different values mean for what you get back.
141
+ """
142
+
143
+ def __init__(
144
+ self,
145
+ api_key: Optional[str] = None,
146
+ secret_key: Optional[str] = None,
147
+ api_version: str = "v1",
148
+ sandbox: bool = True,
149
+ raw_data: bool = False,
150
+ url_override: Optional[str] = None,
151
+ ):
152
+ """
153
+ Args:
154
+ api_key (Optional[str]): Broker API key - set sandbox to true if using sandbox keys. Defaults to None.
155
+ secret_key (Optional[str]): Broker API secret key - set sandbox to true if using sandbox keys. Defaults to None.
156
+ api_version (str): API version. Defaults to 'v1'.
157
+ sandbox (bool): True if using sandbox mode. Defaults to True.
158
+ raw_data (bool): True if you want raw response instead of wrapped responses. Defaults to False.
159
+ This has not been implemented yet.
160
+ url_override (Optional[str]): A url to override and use as the base url.
161
+ """
162
+ base_url = (
163
+ url_override
164
+ if url_override is not None
165
+ else BaseURL.BROKER_SANDBOX.value if sandbox else BaseURL.BROKER_PRODUCTION
166
+ )
167
+
168
+ super().__init__(
169
+ base_url=base_url,
170
+ api_key=api_key,
171
+ secret_key=secret_key,
172
+ api_version=api_version,
173
+ sandbox=sandbox,
174
+ raw_data=raw_data,
175
+ )
176
+
177
+ def _get_auth_headers(self) -> dict:
178
+ # We override this since we use Basic auth
179
+ auth_string = f"{self._api_key}:{self._secret_key}"
180
+ auth_string_encoded = base64.b64encode(str.encode(auth_string))
181
+
182
+ return {"Authorization": "Basic " + auth_string_encoded.decode("utf-8")}
183
+
184
+ def _iterate_over_pages(
185
+ self,
186
+ endpoint: str,
187
+ params: Dict[str, Any],
188
+ response_field: str,
189
+ base_model_type: Type[BaseModel],
190
+ max_items_limit: Optional[int] = None,
191
+ ) -> Iterator[Union[RawData, BaseModel]]:
192
+ """
193
+ Internal method to iterate over the result pages.
194
+ """
195
+
196
+ # we need to track total items retrieved
197
+ total_items = 0
198
+ page_size = params.get("limit", 100)
199
+
200
+ while True:
201
+ if max_items_limit is not None:
202
+ normalized_page_size = min(
203
+ int(max_items_limit) - total_items, page_size
204
+ )
205
+ params["limit"] = normalized_page_size
206
+
207
+ response = self.get(endpoint, params)
208
+ if response is None:
209
+ break
210
+ result = response.get(response_field, None)
211
+
212
+ if not isinstance(result, List) or len(result) == 0:
213
+ break
214
+
215
+ num_items_returned = len(result)
216
+ if (
217
+ max_items_limit is not None
218
+ and num_items_returned + total_items > max_items_limit
219
+ ):
220
+ result = result[: (max_items_limit - total_items)]
221
+ total_items += max_items_limit - total_items
222
+ else:
223
+ total_items += num_items_returned
224
+
225
+ if self._use_raw_data:
226
+ yield result
227
+ else:
228
+ yield TypeAdapter(type=List[base_model_type]).validate_python(result)
229
+
230
+ if max_items_limit is not None and total_items >= max_items_limit:
231
+ break
232
+
233
+ page_token = response.get("next_page_token", None)
234
+
235
+ if page_token is None:
236
+ break
237
+
238
+ params["page_token"] = page_token
239
+
240
+ # ############################## ACCOUNTS/TRADING ACCOUNTS ################################# #
241
+
242
+ def create_account(
243
+ self,
244
+ account_data: CreateAccountRequest,
245
+ ) -> Union[Account, RawData]:
246
+ """
247
+ Create an account.
248
+
249
+ Args:
250
+ account_data (CreateAccountRequest): The data representing the Account you wish to create
251
+
252
+ Returns:
253
+ Account: The newly created Account instance as returned from the API. Should now have id
254
+ and other generated fields filled out.
255
+ """
256
+
257
+ data = account_data.to_request_fields()
258
+ response = self.post("/accounts", data)
259
+
260
+ return Account(**response)
261
+
262
+ def get_account_by_id(
263
+ self,
264
+ account_id: Union[UUID, str],
265
+ ) -> Union[Account, RawData]:
266
+ """
267
+ Get an Account by its associated account_id.
268
+
269
+ Note: If no account is found the api returns a 401, not a 404
270
+
271
+ Args:
272
+ account_id (Union[UUID, str]): The id of the account you wish to get. str uuid values will be upcast
273
+ into UUID instances
274
+
275
+ Returns:
276
+ Account: Returns the requested account.
277
+ """
278
+
279
+ account_id = validate_uuid_id_param(account_id)
280
+
281
+ resp = self.get(f"/accounts/{account_id}", {"params": ""})
282
+ return Account(**resp)
283
+
284
+ def update_account(
285
+ self,
286
+ account_id: Union[UUID, str],
287
+ update_data: UpdateAccountRequest,
288
+ ) -> Union[Account, RawData]:
289
+ """
290
+ Updates data for an account with an id of `account_id`. Note that not all data for an account is modifiable
291
+ after creation so there is a special data type of AccountUpdateRequest representing the data that is
292
+ allowed to be modified.
293
+
294
+ see: https://alpaca.markets/docs/api-references/broker-api/accounts/accounts/#updating-an-account for more info
295
+
296
+ Args:
297
+ account_id (Union[UUID, str]): The id for the account you with to update. str uuid values will be upcast
298
+ into UUID instances
299
+ update_data (UpdateAccountRequest): The data you wish to update this account to
300
+
301
+ Returns:
302
+ Account: Returns an Account instance with the updated data as returned from the api
303
+ """
304
+ account_id = validate_uuid_id_param(account_id)
305
+ params = update_data.to_request_fields()
306
+
307
+ if len(params) < 1:
308
+ raise ValueError("update_data must contain at least 1 field to change")
309
+
310
+ response = self.patch(f"/accounts/{account_id}", params)
311
+
312
+ if self._use_raw_data:
313
+ return response
314
+
315
+ return Account(**response)
316
+
317
+ def delete_account(
318
+ self,
319
+ account_id: Union[UUID, str],
320
+ ) -> None:
321
+ """
322
+ DEPRECATED:
323
+ delete_account is deprecated and will be removed in a future version.
324
+ Please use `close_account(account_id)` instead
325
+
326
+ Args:
327
+ account_id (Union[UUID, str]): The id of the account to be closed
328
+
329
+ Returns:
330
+ None:
331
+ """
332
+ warnings.warn(
333
+ "delete_account is deprecated and will be removed in a future version."
334
+ "Please use `close_account(account_id)` instead",
335
+ DeprecationWarning,
336
+ )
337
+
338
+ self.close_account(account_id)
339
+
340
+ def close_account(
341
+ self,
342
+ account_id: Union[UUID, str],
343
+ ) -> None:
344
+ """
345
+ This operation closes an active account. The underlying records and information of the account are not deleted by this operation.
346
+
347
+ Before closing an account, you are responsible for closing all the positions and withdrawing all the money associated with that account.
348
+
349
+ ref. https://docs.alpaca.markets/reference/post-v1-accounts-account_id-actions-close-1
350
+
351
+ Args:
352
+ account_id (Union[UUID, str]): The id of the account to be closed
353
+
354
+ Returns:
355
+ None:
356
+ """
357
+
358
+ account_id = validate_uuid_id_param(account_id)
359
+
360
+ self.post(f"/accounts/{account_id}/actions/close")
361
+
362
+ def list_accounts(
363
+ self,
364
+ search_parameters: Optional[ListAccountsRequest] = None,
365
+ ) -> Union[List[Account], RawData]:
366
+ """
367
+ Get a List of Accounts allowing for passing in some filters.
368
+
369
+ Args:
370
+ search_parameters (Optional[ListAccountsRequest]): The various filtering criteria you can specify.
371
+
372
+ Returns:
373
+ List[Account]: The filtered list of Accounts
374
+ """
375
+
376
+ params = search_parameters.to_request_fields() if search_parameters else {}
377
+
378
+ # API expects comma separated for entities not multiple params
379
+ if "entities" in params and params["entities"] is not None:
380
+ params["entities"] = ",".join(params["entities"])
381
+
382
+ response = self.get(
383
+ f"/accounts",
384
+ params,
385
+ )
386
+
387
+ if self._use_raw_data:
388
+ return response
389
+ return TypeAdapter(List[Account]).validate_python(response)
390
+
391
+ def get_trade_account_by_id(
392
+ self,
393
+ account_id: Union[UUID, str],
394
+ ) -> Union[TradeAccount, RawData]:
395
+ """
396
+ Gets TradeAccount information for a given Account id.
397
+
398
+ Args:
399
+ account_id (Union[UUID, str]): The UUID identifier for the Account you wish to get the info for. str values
400
+ will be upcast into UUID instances and checked for validity.
401
+
402
+ Returns:
403
+ alpaca.broker.models.accounts.TradeAccount: TradeAccount info for the given account if found.
404
+ """
405
+
406
+ account_id = validate_uuid_id_param(account_id)
407
+
408
+ result = self.get(f"/trading/accounts/{account_id}/account")
409
+
410
+ if self._use_raw_data:
411
+ return result
412
+
413
+ return TradeAccount(**result)
414
+
415
+ def upload_documents_to_account(
416
+ self,
417
+ account_id: Union[UUID, str],
418
+ document_data: List[Union[UploadDocumentRequest, UploadW8BenDocumentRequest]],
419
+ ) -> None:
420
+ """
421
+ Allows you to upload up to 10 documents at a time for an Account.
422
+
423
+ Document data must be a binary objects whose contents are encoded in base64. Each encoded content size is
424
+ limited to 10MB if you use Alpaca for KYCaaS.
425
+
426
+ If you perform your own KYC there are no document size limitations.
427
+
428
+ Args:
429
+ account_id (Union[UUID, str]): The id of the Account you wish to upload the document data to.
430
+ document_data (List[UploadDocumentRequest]): List of UploadDocumentRequest's that contain the relevant
431
+ Document data
432
+
433
+ Returns:
434
+ None: This function returns nothing on success and will raise an APIError in case of a failure
435
+
436
+ Raises:
437
+ APIError: this will be raised if the API didn't return a 204 for your request.
438
+ """
439
+
440
+ account_id = validate_uuid_id_param(account_id)
441
+
442
+ if len(document_data) > BROKER_DOCUMENT_UPLOAD_LIMIT:
443
+ raise ValueError(
444
+ f"document_data cannot be longer than {BROKER_DOCUMENT_UPLOAD_LIMIT}"
445
+ )
446
+
447
+ self.post(
448
+ f"/accounts/{account_id}/documents/upload",
449
+ [document.to_request_fields() for document in document_data],
450
+ )
451
+
452
+ def get_trade_configuration_for_account(
453
+ self,
454
+ account_id: Union[UUID, str],
455
+ ) -> Union[TradeAccountConfiguration, RawData]:
456
+ """
457
+ Gets the TradeAccountConfiguration for a given Account.
458
+
459
+ Args:
460
+ account_id (Union[UUID, str]): The id of the Account you wish to get the TradeAccountConfiguration for
461
+
462
+ Returns:
463
+ TradeAccountConfiguration: The resulting TradeAccountConfiguration for the Account
464
+ """
465
+
466
+ account_id = validate_uuid_id_param(account_id, "account_id")
467
+
468
+ resp = self.get(f"/trading/accounts/{account_id}/account/configurations")
469
+
470
+ if self._use_raw_data:
471
+ return resp
472
+
473
+ return TradeAccountConfiguration(**resp)
474
+
475
+ def update_trade_configuration_for_account(
476
+ self,
477
+ account_id: Union[UUID, str],
478
+ config: TradeAccountConfiguration,
479
+ ) -> Union[TradeAccountConfiguration, RawData]:
480
+ """
481
+ Updates an Account with new TradeAccountConfiguration information.
482
+
483
+ Args:
484
+ account_id (Union[UUID, str]): The id of the Account you wish to update.
485
+ config (UpdateTradeConfigurationRequest): The Updated Options you wish to set on the Account
486
+
487
+ Returns:
488
+ TradeAccountConfiguration: The resulting TradeAccountConfiguration with updates.
489
+ """
490
+ account_id = validate_uuid_id_param(account_id, "account_id")
491
+
492
+ result = self.patch(
493
+ f"/trading/accounts/{account_id}/account/configurations",
494
+ config.model_dump(),
495
+ )
496
+
497
+ if self._use_raw_data:
498
+ return result
499
+
500
+ return TradeAccountConfiguration(**result)
501
+
502
+ def get_cip_data_for_account_by_id(
503
+ self,
504
+ account_id: Union[UUID, str],
505
+ ) -> None:
506
+ """
507
+ Get CIP Info for an account.
508
+
509
+ Args:
510
+ account_id (Union[UUID, str]): The Account id you wish to retrieve CIPInfo for
511
+
512
+ Returns:
513
+ CIPInfo: The CIP info for the Account
514
+ """
515
+ account_id = validate_uuid_id_param(account_id)
516
+ # TODO: can't verify the CIP routes in sandbox they always return 404.
517
+ # Need to ask broker team how we'll even test this
518
+ pass
519
+
520
+ def upload_cip_data_for_account_by_id(
521
+ self,
522
+ account_id: Union[UUID, str],
523
+ ):
524
+ # TODO: can't verify the CIP routes in sandbox they always return 404.
525
+ # Need to ask broker team how we'll even test this
526
+ pass
527
+
528
+ # ############################## ACCOUNT ACTIVITIES ################################# #
529
+
530
+ def get_account_activities(
531
+ self,
532
+ activity_filter: GetAccountActivitiesRequest,
533
+ max_items_limit: Optional[int] = None,
534
+ handle_pagination: Optional[PaginationType] = None,
535
+ ) -> Union[List[BaseActivity], Iterator[List[BaseActivity]]]:
536
+ """
537
+ Gets a list of Account activities, with various filtering options. Please see the documentation for
538
+ GetAccountActivitiesRequest for more information as to what filters are available.
539
+
540
+ The return type of this function is List[BaseActivity] however the list will contain concrete instances of one
541
+ of the child classes of BaseActivity, either TradeActivity or NonTradeActivity. It can be a mixed list depending
542
+ on what filtering criteria you pass through `activity_filter`
543
+
544
+
545
+ Args:
546
+ activity_filter (GetAccountActivitiesRequest): The various filtering fields you can specify to restrict
547
+ results
548
+ max_items_limit (Optional[int]): A maximum number of items to return over all for when handle_pagination is
549
+ of type `PaginationType.FULL`. Ignored otherwise.
550
+ handle_pagination (Optional[PaginationType]): What kind of pagination you want. If None then defaults to
551
+ `PaginationType.FULL`
552
+
553
+ Returns:
554
+ Union[List[BaseActivity], Iterator[List[BaseActivity]]]: Either a list or an Iterator of lists of
555
+ BaseActivity child classes
556
+ """
557
+ handle_pagination = BrokerClient._validate_pagination(
558
+ max_items_limit, handle_pagination
559
+ )
560
+
561
+ # otherwise, user wants pagination so we grab an interator
562
+ iterator = self._get_account_activities_iterator(
563
+ activity_filter=activity_filter,
564
+ max_items_limit=max_items_limit,
565
+ mapping=lambda raw_activities: [
566
+ self._parse_activity(activity) for activity in raw_activities
567
+ ],
568
+ )
569
+
570
+ return BrokerClient._return_paginated_result(iterator, handle_pagination)
571
+
572
+ def _get_account_activities_iterator(
573
+ self,
574
+ activity_filter: GetAccountActivitiesRequest,
575
+ mapping: Callable[[HTTPResult], List[BaseActivity]],
576
+ max_items_limit: Optional[int] = None,
577
+ ) -> Iterator[List[BaseActivity]]:
578
+ """
579
+ Private method for handling the iterator parts of get_account_activities
580
+ """
581
+
582
+ # we need to track total items retrieved
583
+ total_items = 0
584
+ request_fields = activity_filter.to_request_fields()
585
+
586
+ while True:
587
+ """
588
+ we have a couple cases to handle here:
589
+ - max limit isn't set, so just handle normally
590
+ - max is set, and page_size isn't
591
+ - date isn't set. So we'll fall back to the default page size
592
+ - date is set, in this case the api is allowed to not page and return all results. Need to make
593
+ sure only take the we allow for making still a single request here but only taking the items we
594
+ need, in case user wanted only 1 request to happen.
595
+ - max is set, and page_size is also set. Keep track of total_items and run a min check every page to
596
+ see if we need to take less than the page_size items
597
+ """
598
+
599
+ if max_items_limit is not None:
600
+ page_size = (
601
+ activity_filter.page_size
602
+ if activity_filter.page_size is not None
603
+ else ACCOUNT_ACTIVITIES_DEFAULT_PAGE_SIZE
604
+ )
605
+
606
+ normalized_page_size = min(
607
+ int(max_items_limit) - total_items, page_size
608
+ )
609
+
610
+ request_fields["page_size"] = normalized_page_size
611
+
612
+ result = self.get("/accounts/activities", request_fields)
613
+
614
+ # the api returns [] when it's done
615
+
616
+ if not isinstance(result, List) or len(result) == 0:
617
+ break
618
+
619
+ num_items_returned = len(result)
620
+
621
+ # need to handle the case where the api won't page and returns all results, ie `date` is set
622
+ if (
623
+ max_items_limit is not None
624
+ and num_items_returned + total_items > max_items_limit
625
+ ):
626
+ result = result[: (max_items_limit - total_items)]
627
+
628
+ total_items += max_items_limit - total_items
629
+ else:
630
+ total_items += num_items_returned
631
+
632
+ yield mapping(result)
633
+
634
+ if max_items_limit is not None and total_items >= max_items_limit:
635
+ break
636
+
637
+ # ok we made it to the end, we need to ask for the next page of results
638
+ last_result = result[-1]
639
+
640
+ if "id" not in last_result:
641
+ raise APIError(
642
+ "AccountActivity didn't contain an `id` field to use for paginating results"
643
+ )
644
+
645
+ # set the pake token to the id of the last activity so we can get the next page
646
+ request_fields["page_token"] = last_result["id"]
647
+
648
+ @staticmethod
649
+ def _parse_activity(data: dict) -> Union[TradeActivity, NonTradeActivity]:
650
+ """
651
+ We cannot just use TypeAdapter for Activity types since we need to know what child instance to cast it into.
652
+
653
+ So this method does just that.
654
+
655
+ Args:
656
+ data (dict): a dict of raw data to attempt to convert into an Activity instance
657
+
658
+ Raises:
659
+ ValueError: Will raise a ValueError if `data` doesn't contain an `activity_type` field to compare
660
+ """
661
+
662
+ if "activity_type" not in data or data["activity_type"] is None:
663
+ raise ValueError(
664
+ "Failed parsing raw activity data, `activity_type` is not present in fields"
665
+ )
666
+
667
+ if ActivityType.is_str_trade_activity(data["activity_type"]):
668
+ return TypeAdapter(TradeActivity).validate_python(data)
669
+ else:
670
+ return TypeAdapter(NonTradeActivity).validate_python(data)
671
+
672
+ # ############################## TRADE ACCOUNT DOCUMENTS ################################# #
673
+
674
+ def get_trade_documents_for_account(
675
+ self,
676
+ account_id: Union[UUID, str],
677
+ documents_filter: Optional[GetTradeDocumentsRequest] = None,
678
+ ) -> Union[List[TradeDocument], RawData]:
679
+ """
680
+ Gets the list of TradeDocuments for an Account.
681
+
682
+ Args:
683
+ account_id (Union[UUID, str]): The id of the Account you wish to retrieve documents for. str values will
684
+ attempt to be upcast into UUID instances
685
+ documents_filter (Optional[GetTradeDocumentsRequest]): The optional set of filters you can apply to filter the
686
+ returned list.
687
+
688
+ Returns:
689
+ List[TradeDocument]: The filtered list of TradeDocuments
690
+ """
691
+ account_id = validate_uuid_id_param(account_id)
692
+
693
+ result = self.get(
694
+ f"/accounts/{account_id}/documents",
695
+ documents_filter.to_request_fields() if documents_filter else {},
696
+ )
697
+
698
+ if self._use_raw_data:
699
+ return result
700
+
701
+ return TypeAdapter(List[TradeDocument]).validate_python(result)
702
+
703
+ def get_trade_document_for_account_by_id(
704
+ self,
705
+ account_id: Union[UUID, str],
706
+ document_id: Union[UUID, str],
707
+ ) -> Union[TradeDocument, RawData]:
708
+ """
709
+ Gets a single TradeDocument by its id
710
+
711
+ Args:
712
+ account_id (Union[UUID, str]): The id of the Account that owns the document
713
+ document_id (Union[UUID, str]): The id of the TradeDocument
714
+
715
+ Returns:
716
+ TradeDocument: The requested TradeDocument
717
+
718
+ Raises:
719
+ APIError: Will raise an APIError if the account_id or a matching document_id for the account are not found.
720
+ """
721
+
722
+ account_id = validate_uuid_id_param(account_id)
723
+ document_id = validate_uuid_id_param(document_id, "document_id")
724
+
725
+ response = self.get(f"/accounts/{account_id}/documents/{document_id}")
726
+
727
+ if self._use_raw_data:
728
+ return response
729
+
730
+ return TypeAdapter(TradeDocument).validate_python(response)
731
+
732
+ def download_trade_document_for_account_by_id(
733
+ self,
734
+ account_id: Union[UUID, str],
735
+ document_id: Union[UUID, str],
736
+ file_path: str,
737
+ ) -> None:
738
+ """
739
+ Downloads a TradeDocument to `file_path`
740
+
741
+ Args:
742
+ account_id (Union[UUID, str]): ID of the account to pull the document from
743
+ document_id (Union[UUID, str]): ID of the document itself
744
+ file_path (str): A full path for where to save the file to
745
+
746
+ Returns:
747
+ None:
748
+ """
749
+
750
+ account_id = validate_uuid_id_param(account_id)
751
+ document_id = validate_uuid_id_param(document_id, "document_id")
752
+ response: Optional[Response] = None
753
+
754
+ # self.get/post/etc all set follow redirects to false, however API will return a 301 redirect we need to follow,
755
+ # so we just do a raw request
756
+
757
+ # force base_url to be a string value instead of enum name
758
+ base_url = self._base_url + ""
759
+
760
+ target_url = f"{base_url}/{self._api_version}/accounts/{account_id}/documents/{document_id}/download"
761
+ num_tries = 0
762
+
763
+ while num_tries <= self._retry:
764
+ response = self._session.get(
765
+ url=target_url,
766
+ headers=self._get_default_headers(),
767
+ allow_redirects=True,
768
+ stream=True,
769
+ )
770
+ num_tries += 1
771
+
772
+ try:
773
+ response.raise_for_status()
774
+ except HTTPError as http_error:
775
+ if response.status_code in self._retry_codes:
776
+ continue
777
+ if "code" in response.text:
778
+ error = response.json()
779
+ if "code" in error:
780
+ raise APIError(error, http_error)
781
+ else:
782
+ raise http_error
783
+
784
+ # if we got here there were no issues', so response is now a value
785
+ break
786
+
787
+ if response is None:
788
+ # we got here either by error or someone has mis-configured us, so we didn't even try
789
+ raise Exception("Somehow we never made a request for download!")
790
+
791
+ with open(file_path, "wb") as f:
792
+ # we specify chunk_size none which is okay since we set stream to true above, so chunks will be as we
793
+ # receive them from the api
794
+ for chunk in response.iter_content(chunk_size=None):
795
+ f.write(chunk)
796
+
797
+ # ############################## FUNDING ################################# #
798
+
799
+ def create_ach_relationship_for_account(
800
+ self,
801
+ account_id: Union[UUID, str],
802
+ ach_data: Union[CreateACHRelationshipRequest, CreatePlaidRelationshipRequest],
803
+ ) -> Union[ACHRelationship, RawData]:
804
+ """
805
+ Creates a single ACH relationship for the given account.
806
+
807
+ Args:
808
+ account_id (Union[UUID, str]): The ID of the Account that has the ACH Relationship.
809
+ ach_data (Union[CreateACHRelationshipRequest, CreatePlaidRelationshipRequest]): The request data used to
810
+ create the ACH relationship.
811
+
812
+ Returns:
813
+ ACHRelationship: The ACH relationship that was created.
814
+ """
815
+ account_id = validate_uuid_id_param(account_id)
816
+
817
+ if not isinstance(
818
+ ach_data, (CreateACHRelationshipRequest, CreatePlaidRelationshipRequest)
819
+ ):
820
+ raise ValueError(
821
+ f"Request data must either be a CreateACHRelationshipRequest instance, or a "
822
+ f"CreatePlaidRelationshipRequest instance. Got unsupported {type(ach_data)} instead."
823
+ )
824
+
825
+ response = self.post(
826
+ f"/accounts/{account_id}/ach_relationships", ach_data.to_request_fields()
827
+ )
828
+
829
+ if self._use_raw_data:
830
+ return response
831
+
832
+ return ACHRelationship(**response)
833
+
834
+ def get_ach_relationships_for_account(
835
+ self,
836
+ account_id: Union[UUID, str],
837
+ statuses: Optional[List[ACHRelationshipStatus]] = None,
838
+ ) -> Union[List[ACHRelationship], RawData]:
839
+ """
840
+ Gets the ACH relationships for an account.
841
+
842
+ Args:
843
+ account_id (Union[UUID, str]): The ID of the Account to get the ACH relationships for.
844
+ statuses (Optional[List[ACHRelationshipStatus]]): Optionally filter a subset of ACH relationship statuses.
845
+
846
+ Returns:
847
+ List[ACHRelationship]: List of ACH relationships returned by the query.
848
+ """
849
+ account_id = validate_uuid_id_param(account_id)
850
+
851
+ params = {}
852
+ if statuses is not None and len(statuses) != 0:
853
+ params["statuses"] = ",".join(statuses)
854
+
855
+ response = self.get(f"/accounts/{account_id}/ach_relationships", params)
856
+
857
+ if self._use_raw_data:
858
+ return response
859
+
860
+ return TypeAdapter(List[ACHRelationship]).validate_python(response)
861
+
862
+ def delete_ach_relationship_for_account(
863
+ self,
864
+ account_id: Union[UUID, str],
865
+ ach_relationship_id: Union[UUID, str],
866
+ ) -> None:
867
+ """
868
+ Delete an ACH Relation by its ID.
869
+
870
+ As the api itself returns a 204 on success this function returns nothing in the successful case and will raise
871
+ an exception in any other case.
872
+
873
+ Args:
874
+ account_id (Union[UUID, str]): The ID of the Account which has the ACH relationship to be deleted.
875
+ ach_relationship_id (Union[UUID, str]): The ID of the ACH relationship to delete.
876
+ """
877
+ account_id = validate_uuid_id_param(account_id)
878
+ ach_relationship_id = validate_uuid_id_param(
879
+ ach_relationship_id, "ach_relationship_id"
880
+ )
881
+ self.delete(f"/accounts/{account_id}/ach_relationships/{ach_relationship_id}")
882
+
883
+ def create_bank_for_account(
884
+ self,
885
+ account_id: Union[UUID, str],
886
+ bank_data: CreateBankRequest,
887
+ ) -> Union[Bank, RawData]:
888
+ """
889
+ Creates a single bank relationship for the given account.
890
+
891
+ Args:
892
+ account_id (Union[UUID, str]): The ID of the Account to create the bank connection for.
893
+ bank_data (CreateBankRequest): The request data used to create the bank connection.
894
+
895
+ Returns:
896
+ Bank: The Bank that was created.
897
+ """
898
+ account_id = validate_uuid_id_param(account_id)
899
+ response = self.post(
900
+ f"/accounts/{account_id}/recipient_banks", bank_data.to_request_fields()
901
+ )
902
+
903
+ if self._use_raw_data:
904
+ return response
905
+
906
+ return Bank(**response)
907
+
908
+ def get_banks_for_account(
909
+ self,
910
+ account_id: Union[UUID, str],
911
+ ) -> Union[List[Bank], RawData]:
912
+ """
913
+ Gets the Banks for an account.
914
+
915
+ Args:
916
+ account_id (Union[UUID, str]): The ID of the Account to get the Banks for.
917
+
918
+ Returns:
919
+ List[Bank]: List of Banks returned by the query.
920
+ """
921
+ account_id = validate_uuid_id_param(account_id)
922
+ response = self.get(f"/accounts/{account_id}/recipient_banks")
923
+
924
+ if self._use_raw_data:
925
+ return response
926
+
927
+ return TypeAdapter(List[Bank]).validate_python(response)
928
+
929
+ def delete_bank_for_account(
930
+ self,
931
+ account_id: Union[UUID, str],
932
+ bank_id: Union[UUID, str],
933
+ ) -> None:
934
+ """
935
+ Delete a Bank by its ID.
936
+
937
+ As the api itself returns a 204 on success this function returns nothing in the successful case and will raise
938
+ an exception in any other case.
939
+
940
+ Args:
941
+ account_id (Union[UUID, str]): The ID of the Account which has the Bank to be deleted.
942
+ bank_id (Union[UUID, str]): The ID of the Bank to delete.
943
+ """
944
+ account_id = validate_uuid_id_param(account_id)
945
+ bank_id = validate_uuid_id_param(bank_id, "bank_id")
946
+ self.delete(f"/accounts/{account_id}/recipient_banks/{bank_id}")
947
+
948
+ def create_transfer_for_account(
949
+ self,
950
+ account_id: Union[UUID, str],
951
+ transfer_data: Union[CreateACHTransferRequest, CreateBankTransferRequest],
952
+ ) -> Union[Transfer, RawData]:
953
+ """
954
+ Creates a single Transfer for the given account.
955
+
956
+ Args:
957
+ account_id (Union[UUID, str]): The ID of the Account to create the bank connection for.
958
+ transfer_data (Union[CreateACHTransferRequest, CreateBankTransferRequest]): The request data used to
959
+ create the bank connection.
960
+
961
+ Returns:
962
+ Transfer: The Transfer that was created.
963
+ """
964
+ account_id = validate_uuid_id_param(account_id)
965
+ response = self.post(
966
+ f"/accounts/{account_id}/transfers", transfer_data.to_request_fields()
967
+ )
968
+
969
+ if self._use_raw_data:
970
+ return response
971
+
972
+ return Transfer(**response)
973
+
974
+ def get_transfers_for_account(
975
+ self,
976
+ account_id: Union[UUID, str],
977
+ transfers_filter: Optional[GetTransfersRequest] = None,
978
+ max_items_limit: Optional[int] = None,
979
+ handle_pagination: Optional[PaginationType] = None,
980
+ ) -> Union[List[Transfer], Iterator[List[Transfer]]]:
981
+ """
982
+ Gets the transfers for an account.
983
+
984
+ Args:
985
+ account_id (Union[UUID, str]): The ID of the Account to create the bank connection for.
986
+ transfers_filter (Optional[GetTransferRequest]): The various filtering parameters to apply to the request.
987
+ max_items_limit (Optional[int]): A maximum number of items to return over all for when handle_pagination is
988
+ of type `PaginationType.FULL`. Ignored otherwise.
989
+ handle_pagination (Optional[PaginationType]): What kind of pagination you want. If None then defaults to
990
+ `PaginationType.FULL`.
991
+
992
+ Returns:
993
+ Union[List[Transfer], Iterator[List[Transfer]]]: Either a list or an Iterator of lists of Transfer child
994
+ classes.
995
+ """
996
+ account_id = validate_uuid_id_param(account_id)
997
+ handle_pagination = BrokerClient._validate_pagination(
998
+ max_items_limit, handle_pagination
999
+ )
1000
+
1001
+ iterator = self._get_transfers_iterator(
1002
+ account_id=account_id,
1003
+ transfers_filter=(
1004
+ transfers_filter
1005
+ if transfers_filter is not None
1006
+ else GetTransfersRequest()
1007
+ ),
1008
+ max_items_limit=max_items_limit,
1009
+ )
1010
+
1011
+ return BrokerClient._return_paginated_result(iterator, handle_pagination)
1012
+
1013
+ def _get_transfers_iterator(
1014
+ self,
1015
+ account_id: UUID,
1016
+ transfers_filter: GetTransfersRequest,
1017
+ max_items_limit: Optional[int],
1018
+ ) -> Iterator[List[Transfer]]:
1019
+ """
1020
+ Private method for handling the iterator parts of get_transfers_for_account.
1021
+ """
1022
+ # We need to track total items retrieved.
1023
+ total_items = 0
1024
+ request_fields = transfers_filter.to_request_fields()
1025
+
1026
+ while True:
1027
+ request_fields["offset"] = total_items
1028
+ result = self.get(f"/accounts/{account_id}/transfers", request_fields)
1029
+
1030
+ # The api returns [] when it's done.
1031
+ if not isinstance(result, List) or len(result) == 0:
1032
+ break
1033
+
1034
+ num_items_returned = len(result)
1035
+
1036
+ if (
1037
+ max_items_limit is not None
1038
+ and num_items_returned + total_items > max_items_limit
1039
+ ):
1040
+ result = result[: (max_items_limit - total_items)]
1041
+ total_items += max_items_limit - total_items
1042
+ else:
1043
+ total_items += num_items_returned
1044
+
1045
+ yield TypeAdapter(List[Transfer]).validate_python(result)
1046
+
1047
+ if max_items_limit is not None and total_items >= max_items_limit:
1048
+ break
1049
+
1050
+ def cancel_transfer_for_account(
1051
+ self,
1052
+ account_id: Union[UUID, str],
1053
+ transfer_id: Union[UUID, str],
1054
+ ) -> None:
1055
+ """
1056
+ Cancel a Transfer by its ID.
1057
+
1058
+ As the api itself returns a 204 on success this function returns nothing in the successful case and will raise
1059
+ an exception in any other case.
1060
+
1061
+ Args:
1062
+ account_id (Union[UUID, str]): The ID of the Account which has the Transfer to be canceled.
1063
+ transfer_id (Union[UUID, str]): The ID of the Transfer to cancel.
1064
+ """
1065
+ account_id = validate_uuid_id_param(account_id)
1066
+ transfer_id = validate_uuid_id_param(transfer_id, "transfer_id")
1067
+ self.delete(f"/accounts/{account_id}/transfers/{transfer_id}")
1068
+
1069
+ # ############################## TRADING ################################# #
1070
+
1071
+ def get_all_positions_for_account(
1072
+ self,
1073
+ account_id: Union[UUID, str],
1074
+ ) -> Union[List[Position], RawData]:
1075
+ """
1076
+ Gets all the current positions for an account.
1077
+
1078
+ Args:
1079
+ account_id (Union[UUID, str]): The ID of the Account to get the open positions for.
1080
+
1081
+ Returns:
1082
+ List[Position]: List of open positions from the account.
1083
+ """
1084
+ account_id = validate_uuid_id_param(account_id)
1085
+ response = self.get(f"/trading/accounts/{account_id}/positions")
1086
+
1087
+ if self._use_raw_data:
1088
+ return response
1089
+ return TypeAdapter(List[Position]).validate_python(response)
1090
+
1091
+ def get_all_accounts_positions(
1092
+ self,
1093
+ ) -> Union[AllAccountsPositions, RawData]:
1094
+ """
1095
+ Gets all the current positions for every account in bulk.
1096
+
1097
+ Returns:
1098
+ AllAccountsPositions: The collection of open positions keyed by account_id.
1099
+ """
1100
+ response = self.get("/accounts/positions")
1101
+
1102
+ if self._use_raw_data:
1103
+ return response
1104
+
1105
+ return AllAccountsPositions(**response)
1106
+
1107
+ def get_open_position_for_account(
1108
+ self, account_id: Union[UUID, str], symbol_or_asset_id: Union[UUID, str]
1109
+ ) -> Union[Position, RawData]:
1110
+ """
1111
+ Gets the open position for an account for a single asset. Throws an APIError if the position does not exist.
1112
+
1113
+ Args:
1114
+ account_id (Union[UUID, str]): The ID of the Account to get the open position for.
1115
+ symbol_or_asset_id (Union[UUID, str]): The symbol name of asset id of the position to get from the account.
1116
+
1117
+ Returns:
1118
+ Position: Open position of the asset from the account.
1119
+ """
1120
+ account_id = validate_uuid_id_param(account_id)
1121
+ symbol_or_asset_id = validate_symbol_or_asset_id(symbol_or_asset_id)
1122
+ response = self.get(
1123
+ f"/trading/accounts/{account_id}/positions/{symbol_or_asset_id}"
1124
+ )
1125
+
1126
+ if self._use_raw_data:
1127
+ return response
1128
+
1129
+ return Position(**response)
1130
+
1131
+ def close_all_positions_for_account(
1132
+ self,
1133
+ account_id: Union[UUID, str],
1134
+ cancel_orders: Optional[bool] = None,
1135
+ ) -> Union[List[ClosePositionResponse], RawData]:
1136
+ """
1137
+ Liquidates all positions for an account.
1138
+
1139
+ Places an order for each open position to liquidate.
1140
+
1141
+ Args:
1142
+ account_id (Union[UUID, str]): The ID of the Account to close the positions for.
1143
+ cancel_orders (Optional[bool]): If true is specified, cancel all open orders before liquidating all positions.
1144
+
1145
+ Returns:
1146
+ List[ClosePositionResponse]: A list of responses from each closed position containing the status code and
1147
+ order id.
1148
+ """
1149
+ account_id = validate_uuid_id_param(account_id)
1150
+ response = self.delete(
1151
+ f"/trading/accounts/{account_id}/positions",
1152
+ {"cancel_orders": cancel_orders} if cancel_orders else None,
1153
+ )
1154
+
1155
+ if self._use_raw_data:
1156
+ return response
1157
+ return TypeAdapter(List[ClosePositionResponse]).validate_python(response)
1158
+
1159
+ def close_position_for_account(
1160
+ self,
1161
+ account_id: Union[UUID, str],
1162
+ symbol_or_asset_id: Union[UUID, str],
1163
+ close_options: Optional[ClosePositionRequest] = None,
1164
+ ) -> Union[Order, RawData]:
1165
+ """
1166
+ Liquidates the position for an account for a single asset.
1167
+
1168
+ Places a single order to close the position for the asset.
1169
+
1170
+ Args:
1171
+ account_id (Union[UUID, str]): The ID of the Account to close the position for.
1172
+ symbol_or_asset_id (Union[UUID, str]): The symbol name of asset id of the position to close on the account.
1173
+ close_options: The various close position request parameters.
1174
+
1175
+ Returns:
1176
+ alpaca.broker.models.Order: The order that was placed to close the position.
1177
+ """
1178
+ account_id = validate_uuid_id_param(account_id)
1179
+ symbol_or_asset_id = validate_symbol_or_asset_id(symbol_or_asset_id)
1180
+ response = self.delete(
1181
+ f"/trading/accounts/{account_id}/positions/{symbol_or_asset_id}",
1182
+ close_options.to_request_fields() if close_options else {},
1183
+ )
1184
+
1185
+ if self._use_raw_data:
1186
+ return response
1187
+
1188
+ return Order(**response)
1189
+
1190
+ def get_portfolio_history_for_account(
1191
+ self,
1192
+ account_id: Union[UUID, str],
1193
+ history_filter: Optional[GetPortfolioHistoryRequest] = None,
1194
+ ) -> Union[PortfolioHistory, RawData]:
1195
+ """
1196
+ Gets the portfolio history statistics for an account.
1197
+
1198
+ Args:
1199
+ account_id (Union[UUID, str]): The ID of the Account to get the portfolio history for.
1200
+ history_filter: The various portfolio history request parameters.
1201
+
1202
+ Returns:
1203
+ PortfolioHistory: The portfolio history statistics for the account.
1204
+ """
1205
+ account_id = validate_uuid_id_param(account_id)
1206
+
1207
+ response = self.get(
1208
+ f"/trading/accounts/{account_id}/account/portfolio/history",
1209
+ history_filter.to_request_fields() if history_filter else {},
1210
+ )
1211
+
1212
+ if self._use_raw_data:
1213
+ return response
1214
+
1215
+ return PortfolioHistory(**response)
1216
+
1217
+ # ############################## CLOCK & CALENDAR ################################# #
1218
+
1219
+ def get_clock(self) -> Union[Clock, RawData]:
1220
+ """
1221
+ Gets the current market timestamp, whether or not the market is currently open, as well as the times
1222
+ of the next market open and close.
1223
+
1224
+ Returns:
1225
+ Clock: The market Clock data
1226
+ """
1227
+
1228
+ response = self.get("/clock")
1229
+
1230
+ if self._use_raw_data:
1231
+ return response
1232
+
1233
+ return Clock(**response)
1234
+
1235
+ def get_calendar(
1236
+ self,
1237
+ filters: Optional[GetCalendarRequest] = None,
1238
+ ) -> Union[List[Calendar], RawData]:
1239
+ """
1240
+ The calendar API serves the full list of market days from 1970 to 2029. It can also be queried by specifying a
1241
+ start and/or end time to narrow down the results.
1242
+
1243
+ In addition to the dates, the response also contains the specific open and close times for the market days,
1244
+ taking into account early closures.
1245
+
1246
+ Args:
1247
+ filters: Any optional filters to limit the returned market days
1248
+
1249
+ Returns:
1250
+ List[Calendar]: A list of Calendar objects representing the market days.
1251
+ """
1252
+
1253
+ result = self.get(
1254
+ "/calendar", filters.to_request_fields() if filters is not None else {}
1255
+ )
1256
+
1257
+ if self._use_raw_data:
1258
+ return result
1259
+
1260
+ return TypeAdapter(List[Calendar]).validate_python(result)
1261
+
1262
+ # ############################## WATCHLISTS ################################# #
1263
+
1264
+ def get_watchlists_for_account(
1265
+ self,
1266
+ account_id: Union[UUID, str],
1267
+ ) -> Union[List[Watchlist], RawData]:
1268
+ """
1269
+ Returns all watchlists for an account.
1270
+
1271
+ Args:
1272
+ account_id (Union[UUID, str]): The account to retrieve watchlists for
1273
+
1274
+ Returns:
1275
+ List[Watchlist]: The watchlists for that account
1276
+ """
1277
+ account_id = validate_uuid_id_param(account_id, "account_id")
1278
+
1279
+ result = self.get(f"/trading/accounts/{account_id}/watchlists")
1280
+
1281
+ if self._use_raw_data:
1282
+ return result
1283
+
1284
+ return TypeAdapter(List[Watchlist]).validate_python(result)
1285
+
1286
+ def get_watchlist_for_account_by_id(
1287
+ self,
1288
+ account_id: Union[UUID, str],
1289
+ watchlist_id: Union[UUID, str],
1290
+ ) -> Union[Watchlist, RawData]:
1291
+ """
1292
+ Returns a specific watchlist by its id for a given account.
1293
+
1294
+ Args:
1295
+ account_id (Union[UUID, str]): The account to retrieve watchlist data for.
1296
+ watchlist_id (Union[UUID, str]): The watchlist to retrieve.
1297
+
1298
+ Returns:
1299
+ Watchlist: The watchlist.
1300
+ """
1301
+ account_id = validate_uuid_id_param(account_id, "account_id")
1302
+ watchlist_id = validate_uuid_id_param(watchlist_id, "watchlist_id")
1303
+
1304
+ result = self.get(f"/trading/accounts/{account_id}/watchlists/{watchlist_id}")
1305
+
1306
+ if self._use_raw_data:
1307
+ return result
1308
+
1309
+ return Watchlist(**result)
1310
+
1311
+ def create_watchlist_for_account(
1312
+ self,
1313
+ account_id: Union[UUID, str],
1314
+ watchlist_data: CreateWatchlistRequest,
1315
+ ) -> Union[Watchlist, RawData]:
1316
+ """
1317
+ Creates a new watchlist for a given account.
1318
+
1319
+ Args:
1320
+ account_id (Union[UUID, str]): The account to create a new watchlist for.
1321
+ watchlist_data (CreateWatchlistRequest): The watchlist to create.
1322
+
1323
+ Returns:
1324
+ Watchlist: The new watchlist.
1325
+ """
1326
+ account_id = validate_uuid_id_param(account_id, "account_id")
1327
+
1328
+ result = self.post(
1329
+ f"/trading/accounts/{account_id}/watchlists",
1330
+ watchlist_data.to_request_fields(),
1331
+ )
1332
+
1333
+ if self._use_raw_data:
1334
+ return result
1335
+
1336
+ return Watchlist(**result)
1337
+
1338
+ def update_watchlist_for_account_by_id(
1339
+ self,
1340
+ account_id: Union[UUID, str],
1341
+ watchlist_id: Union[UUID, str],
1342
+ # Might be worth taking a union of this and Watchlist itself; but then we should make a change like that SDK
1343
+ # wide. Probably a good 0.2.x change
1344
+ watchlist_data: UpdateWatchlistRequest,
1345
+ ) -> Union[Watchlist, RawData]:
1346
+ """
1347
+ Updates a watchlist with new data.
1348
+
1349
+ Args:
1350
+ account_id (Union[UUID, str]): The account whose watchlist to be updated.
1351
+ watchlist_id (Union[UUID, str]): The watchlist to be updated.
1352
+ watchlist_data (UpdateWatchlistRequest): The new watchlist data.
1353
+
1354
+ Returns:
1355
+ Watchlist: The watchlist with updated data.
1356
+ """
1357
+ watchlist_id = validate_uuid_id_param(watchlist_id, "watchlist_id")
1358
+ account_id = validate_uuid_id_param(account_id, "account_id")
1359
+
1360
+ result = self.put(
1361
+ f"/trading/accounts/{account_id}/watchlists/{watchlist_id}",
1362
+ watchlist_data.to_request_fields(),
1363
+ )
1364
+
1365
+ if self._use_raw_data:
1366
+ return result
1367
+
1368
+ return Watchlist(**result)
1369
+
1370
+ def add_asset_to_watchlist_for_account_by_id(
1371
+ self,
1372
+ account_id: Union[UUID, str],
1373
+ watchlist_id: Union[UUID, str],
1374
+ symbol: str,
1375
+ ) -> Union[Watchlist, RawData]:
1376
+ """
1377
+ Adds an asset by its symbol to a specified watchlist for a given account.
1378
+ Args:
1379
+ account_id (Union[UUID, str]): The account id that the watchlist belongs to.
1380
+ watchlist_id (Union[UUID, str]): The watchlist to add the symbol to.
1381
+ symbol (str): The symbol for the asset to add.
1382
+
1383
+ Returns:
1384
+ Watchlist: The updated watchlist.
1385
+ """
1386
+ account_id = validate_uuid_id_param(account_id, "account_id")
1387
+ watchlist_id = validate_uuid_id_param(watchlist_id, "watchlist_id")
1388
+
1389
+ params = {"symbol": symbol}
1390
+
1391
+ result = self.post(
1392
+ f"/trading/accounts/{account_id}/watchlists/{watchlist_id}", params
1393
+ )
1394
+
1395
+ if self._use_raw_data:
1396
+ return result
1397
+
1398
+ return Watchlist(**result)
1399
+
1400
+ def delete_watchlist_from_account_by_id(
1401
+ self,
1402
+ account_id: Union[UUID, str],
1403
+ watchlist_id: Union[UUID, str],
1404
+ ) -> None:
1405
+ """
1406
+ Deletes a watchlist. This is permanent.
1407
+
1408
+ Args:
1409
+ account_id (Union[UUID, str]): The account the watchlist belongs to.
1410
+ watchlist_id (Union[UUID, str]): The watchlist to delete.
1411
+
1412
+ Returns:
1413
+ None
1414
+ """
1415
+ account_id = validate_uuid_id_param(account_id, "account_id")
1416
+ watchlist_id = validate_uuid_id_param(watchlist_id, "watchlist_id")
1417
+
1418
+ self.delete(f"/trading/accounts/{account_id}/watchlists/{watchlist_id}")
1419
+
1420
+ def remove_asset_from_watchlist_for_account_by_id(
1421
+ self,
1422
+ account_id: Union[UUID, str],
1423
+ watchlist_id: Union[UUID, str],
1424
+ symbol: str,
1425
+ ) -> Union[Watchlist, RawData]:
1426
+ """
1427
+ Removes an asset from a watchlist for a given account.
1428
+
1429
+ Args:
1430
+ account_id (Union[UUID, str]): The account the watchlist belongs to.
1431
+ watchlist_id (Union[UUID, str]): The watchlist to remove the asset from.
1432
+ symbol (str): The symbol for the asset to add.
1433
+
1434
+ Returns:
1435
+ Watchlist: The updated watchlist.
1436
+ """
1437
+ account_id = validate_uuid_id_param(account_id, "account_id")
1438
+ watchlist_id = validate_uuid_id_param(watchlist_id, "watchlist_id")
1439
+
1440
+ result = self.delete(
1441
+ f"/trading/accounts/{account_id}/watchlists/{watchlist_id}/{symbol}"
1442
+ )
1443
+
1444
+ if self._use_raw_data:
1445
+ return result
1446
+
1447
+ return Watchlist(**result)
1448
+
1449
+ # ############################## JOURNALS ################################# #
1450
+
1451
+ def create_journal(
1452
+ self,
1453
+ journal_data: CreateJournalRequest,
1454
+ ) -> Union[Journal, RawData]:
1455
+ """
1456
+ The journal API allows you to transfer cash and securities between accounts.
1457
+
1458
+ Creates a new journal request.
1459
+
1460
+ Args:
1461
+ journal_data (CreateJournalRequest): THe journal to be submitted.
1462
+
1463
+ Returns:
1464
+ Journal: The submitted journal.
1465
+ """
1466
+ params = journal_data.to_request_fields() if journal_data else {}
1467
+
1468
+ response = self.post("/journals", params)
1469
+
1470
+ if self._use_raw_data:
1471
+ return response
1472
+
1473
+ return Journal(**response)
1474
+
1475
+ def create_batch_journal(
1476
+ self,
1477
+ batch_data: CreateBatchJournalRequest,
1478
+ ) -> Union[List[BatchJournalResponse], RawData]:
1479
+ """
1480
+ A batch journal moves assets from one account into many others.
1481
+
1482
+ Currently, cash batch journals are supported.
1483
+
1484
+ Args:
1485
+ batch_data (CreateBatchJournalRequest): The batch journals to be submitted.
1486
+
1487
+ Returns:
1488
+ BatchJournalResponse: The submitted batch journals.
1489
+ """
1490
+ params = batch_data.to_request_fields() if batch_data else {}
1491
+
1492
+ response = self.post("/journals/batch", params)
1493
+
1494
+ if self._use_raw_data:
1495
+ return response
1496
+
1497
+ return TypeAdapter(List[BatchJournalResponse]).validate_python(response)
1498
+
1499
+ def create_reverse_batch_journal(
1500
+ self,
1501
+ reverse_batch_data: CreateReverseBatchJournalRequest,
1502
+ ) -> Union[List[BatchJournalResponse], RawData]:
1503
+ """
1504
+ A reverse batch journal moves assets into one account from many others.
1505
+
1506
+ Currently, cash reverse batch journals are supported.
1507
+
1508
+ Args:
1509
+ reverse_batch_data (CreateReverseBatchJournalRequest): The reverse batch journals to be submitted.
1510
+
1511
+ Returns:
1512
+ BatchJournalResponse: The submitted reverse batch journals.
1513
+ """
1514
+ params = reverse_batch_data.to_request_fields() if reverse_batch_data else {}
1515
+
1516
+ response = self.post("/journals/reverse_batch", params)
1517
+
1518
+ if self._use_raw_data:
1519
+ return response
1520
+
1521
+ return TypeAdapter(List[BatchJournalResponse]).validate_python(response)
1522
+
1523
+ def get_journals(
1524
+ self, journal_filter: Optional[GetJournalsRequest] = None
1525
+ ) -> Union[List[Journal], RawData]:
1526
+ """
1527
+ Returns journals from the master list.
1528
+
1529
+ Args:
1530
+ journal_filter (Optional[GetJournalsRequest]): The parameters to filter the query by.
1531
+
1532
+ Returns:
1533
+ List[Journal]: The journals from the query.
1534
+ """
1535
+ params = journal_filter.to_request_fields() if journal_filter else {}
1536
+
1537
+ response = self.get("/journals", params)
1538
+
1539
+ if self._use_raw_data:
1540
+ return response
1541
+
1542
+ return TypeAdapter(List[Journal]).validate_python(response)
1543
+
1544
+ def get_journal_by_id(
1545
+ self, journal_id: Union[UUID, str] = None
1546
+ ) -> Union[Journal, RawData]:
1547
+ """
1548
+ Returns a specific journal by its id.
1549
+
1550
+ Args:
1551
+ journal_id (Union[UUID, str]): The id of the journal to retrieve.
1552
+
1553
+ Returns:
1554
+ Journal: The journal with given id.
1555
+ """
1556
+ journal_id = validate_uuid_id_param(journal_id, "journal id")
1557
+
1558
+ response = self.get(f"/journals/{journal_id}")
1559
+
1560
+ if self._use_raw_data:
1561
+ return response
1562
+
1563
+ return Journal(**response)
1564
+
1565
+ def cancel_journal_by_id(
1566
+ self,
1567
+ journal_id: Union[UUID, str],
1568
+ ) -> None:
1569
+ """
1570
+ Cancels a specific journal by its id.
1571
+
1572
+ Args:
1573
+ journal_id (Union[UUID, str]): The id of the journal to be cancelled.
1574
+
1575
+ Returns:
1576
+ None
1577
+ """
1578
+ journal_id = validate_uuid_id_param(journal_id, "journal id")
1579
+
1580
+ self.delete(f"/journals/{journal_id}")
1581
+
1582
+ # ############################## Assets ################################# #
1583
+
1584
+ def get_all_assets(
1585
+ self, filter: Optional[GetAssetsRequest] = None
1586
+ ) -> Union[List[Asset], RawData]:
1587
+ """
1588
+ The assets API serves as the master list of assets available for trade and data consumption from Alpaca.
1589
+ Some assets are not tradable with Alpaca. These assets will be marked with the flag tradable=false.
1590
+
1591
+ Args:
1592
+ filter (Optional[GetAssetsRequest]): The parameters that can be assets can be queried by.
1593
+
1594
+ Returns:
1595
+ List[Asset]: The list of assets.
1596
+ """
1597
+ # checking to see if we specified at least one param
1598
+ params = filter.to_request_fields() if filter is not None else {}
1599
+
1600
+ response = self.get(f"/assets", params)
1601
+
1602
+ if self._use_raw_data:
1603
+ return response
1604
+
1605
+ return TypeAdapter(
1606
+ List[Asset],
1607
+ ).validate_python(response)
1608
+
1609
+ def get_asset(self, symbol_or_asset_id: Union[UUID, str]) -> Union[Asset, RawData]:
1610
+ """
1611
+ Returns a specific asset by its symbol or asset id. If the specified asset does not exist
1612
+ a 404 error will be thrown.
1613
+
1614
+ Args:
1615
+ symbol_or_asset_id (Union[UUID, str]): The symbol or asset id for the specified asset
1616
+
1617
+ Returns:
1618
+ Asset: The asset if it exists.
1619
+ """
1620
+
1621
+ symbol_or_asset_id = validate_symbol_or_asset_id(symbol_or_asset_id)
1622
+
1623
+ response = self.get(f"/assets/{symbol_or_asset_id}")
1624
+
1625
+ if self._use_raw_data:
1626
+ return response
1627
+
1628
+ return Asset(**response)
1629
+
1630
+ # ############################## ORDERS ################################# #
1631
+
1632
+ def submit_order_for_account(
1633
+ self, account_id: Union[UUID, str], order_data: OrderRequest
1634
+ ) -> Union[Order, RawData]:
1635
+ """Creates an order to buy or sell an asset for an account.
1636
+
1637
+ Args:
1638
+ account_id (Union[UUID, str]): The account the order will be created for.
1639
+ order_data (alpaca.broker.requests.OrderRequest): The request data for creating a new order.
1640
+
1641
+ Returns:
1642
+ alpaca.broker.models.OrderOrder: The resulting submitted order.
1643
+ """
1644
+
1645
+ account_id = validate_uuid_id_param(account_id, "account_id")
1646
+
1647
+ data = order_data.to_request_fields()
1648
+
1649
+ response = self.post(f"/trading/accounts/{account_id}/orders", data)
1650
+
1651
+ if self._use_raw_data:
1652
+ return response
1653
+
1654
+ return Order(**response)
1655
+
1656
+ def get_orders_for_account(
1657
+ self, account_id: Union[UUID, str], filter: Optional[GetOrdersRequest] = None
1658
+ ) -> Union[List[Order], RawData]:
1659
+ """
1660
+ Returns all orders for an account. Orders can be filtered by parameters.
1661
+
1662
+ Args:
1663
+ account_id (Union[UUID, str]): The account to get the orders for.
1664
+ filter (Optional[GetOrdersRequest]): The parameters to filter the orders with.
1665
+
1666
+ Returns:
1667
+ List[alpaca.broker.models.Order]: The queried orders.
1668
+ """
1669
+ account_id = validate_uuid_id_param(account_id, "account_id")
1670
+
1671
+ # checking to see if we specified at least one param
1672
+ params = filter.to_request_fields() if filter is not None else {}
1673
+
1674
+ if "symbols" in params and isinstance(params["symbols"], list):
1675
+ params["symbols"] = ",".join(params["symbols"])
1676
+
1677
+ response = self.get(f"/trading/accounts/{account_id}/orders", params)
1678
+
1679
+ if self._use_raw_data:
1680
+ return response
1681
+
1682
+ return TypeAdapter(
1683
+ List[Order],
1684
+ ).validate_python(response)
1685
+
1686
+ def get_order_for_account_by_id(
1687
+ self,
1688
+ account_id: Union[UUID, str],
1689
+ order_id: Union[UUID, str],
1690
+ filter: Optional[GetOrderByIdRequest] = None,
1691
+ ) -> Union[Order, RawData]:
1692
+ """
1693
+ Returns a specific order by its order id.
1694
+
1695
+ Args:
1696
+ account_id (Union[UUID, str]): The account to get the order for.
1697
+ order_id (Union[UUID, str]): The unique uuid identifier for the order.
1698
+ filter (Optional[GetOrderByIdRequest]): The parameters for the query.
1699
+
1700
+ Returns:
1701
+ alpaca.broker.models.Order: The order that was queried.
1702
+ """
1703
+ account_id = validate_uuid_id_param(account_id, "account_id")
1704
+ order_id = validate_uuid_id_param(order_id, "order_id")
1705
+
1706
+ # checking to see if we specified at least one param
1707
+ params = filter.to_request_fields() if filter is not None else {}
1708
+
1709
+ response = self.get(f"/trading/accounts/{account_id}/orders/{order_id}", params)
1710
+
1711
+ if self._use_raw_data:
1712
+ return response
1713
+
1714
+ return Order(**response)
1715
+
1716
+ def get_order_for_account_by_client_id(
1717
+ self, account_id: Union[UUID, str], client_id: str
1718
+ ) -> Union[Order, RawData]:
1719
+ """
1720
+ Returns a specific order by its client order id.
1721
+
1722
+ Args:
1723
+ account_id (Union[UUID, str]): The account to get the order for.
1724
+ client_id (str): The client order identifier for the order.
1725
+
1726
+ Returns:
1727
+ alpaca.broker.models.Order: The queried order.
1728
+ """
1729
+ account_id = validate_uuid_id_param(account_id, "account_id")
1730
+
1731
+ params = {"client_order_id": client_id}
1732
+
1733
+ response = self.get(
1734
+ f"/trading/accounts/{account_id}/orders:by_client_order_id", params
1735
+ )
1736
+
1737
+ if self._use_raw_data:
1738
+ return response
1739
+
1740
+ return Order(**response)
1741
+
1742
+ def replace_order_for_account_by_id(
1743
+ self,
1744
+ account_id: Union[UUID, str],
1745
+ order_id: Union[UUID, str],
1746
+ order_data: Optional[ReplaceOrderRequest] = None,
1747
+ ) -> Union[Order, RawData]:
1748
+ """
1749
+ Updates an order with new parameters.
1750
+
1751
+ Args:
1752
+ account_id (Union[UUID, str]): The account to replace the order for.
1753
+ order_id (Union[UUID, str]): The unique uuid identifier for the order being replaced.
1754
+ order_data (Optional[ReplaceOrderRequest]): The parameters we wish to update.
1755
+
1756
+ Returns:
1757
+ alpaca.broker.models.Order: The updated order.
1758
+ """
1759
+ account_id = validate_uuid_id_param(account_id, "account_id")
1760
+ order_id = validate_uuid_id_param(order_id, "order_id")
1761
+
1762
+ # checking to see if we specified at least one param
1763
+ params = order_data.to_request_fields() if order_data is not None else {}
1764
+
1765
+ response = self.patch(
1766
+ f"/trading/accounts/{account_id}/orders/{order_id}", params
1767
+ )
1768
+
1769
+ if self._use_raw_data:
1770
+ return response
1771
+
1772
+ return Order(**response)
1773
+
1774
+ def cancel_orders_for_account(
1775
+ self, account_id: Union[UUID, str]
1776
+ ) -> Union[List[CancelOrderResponse], RawData]:
1777
+ """
1778
+ Cancels all orders.
1779
+
1780
+ Args:
1781
+ account_id (Union[UUID, str]): The account to cancel the orders for.
1782
+
1783
+ Returns:
1784
+ List[CancelOrderResponse]: The list of HTTP statuses for each order attempted to be cancelled.
1785
+ """
1786
+ account_id = validate_uuid_id_param(account_id, "account_id")
1787
+
1788
+ response = self.delete(f"/trading/accounts/{account_id}/orders")
1789
+
1790
+ if self._use_raw_data:
1791
+ return response
1792
+
1793
+ return TypeAdapter(
1794
+ List[CancelOrderResponse],
1795
+ ).validate_python(response)
1796
+
1797
+ def cancel_order_for_account_by_id(
1798
+ self, account_id: Union[UUID, str], order_id: Union[UUID, str]
1799
+ ) -> None:
1800
+ """
1801
+ Cancels a specific order by its order id.
1802
+
1803
+ Args:
1804
+ account_id (Union[UUID, str]): The account to cancel the order for.
1805
+ order_id (Union[UUID, str]): The unique uuid identifier of the order being cancelled.
1806
+
1807
+ """
1808
+ account_id = validate_uuid_id_param(account_id, "account_id")
1809
+ order_id = validate_uuid_id_param(order_id, "order_id")
1810
+
1811
+ # TODO: Should ideally return some information about the order's cancel status (Issue #78)
1812
+ # TODO: Currently no way to retrieve status details for empty responses with base REST implementation
1813
+ self.delete(f"/trading/accounts/{account_id}/orders/{order_id}")
1814
+
1815
+ # ############################## CORPORATE ACTIONS ################################# #
1816
+
1817
+ def get_corporate_announcements(
1818
+ self, filter: GetCorporateAnnouncementsRequest
1819
+ ) -> Union[List[CorporateActionAnnouncement], RawData]:
1820
+ """
1821
+ Returns corporate action announcements data given specified search criteria.
1822
+ Args:
1823
+ filter (GetCorporateAnnouncementsRequest): The parameters to filter the search by.
1824
+ Returns:
1825
+ List[CorporateActionAnnouncement]: The resulting announcements from the search.
1826
+ """
1827
+ params = filter.to_request_fields() if filter else {}
1828
+
1829
+ if "ca_types" in params and isinstance(params["ca_types"], list):
1830
+ params["ca_types"] = ",".join(params["ca_types"])
1831
+
1832
+ response = self.get("/corporate_actions/announcements", params)
1833
+
1834
+ if self._use_raw_data:
1835
+ return response
1836
+
1837
+ return TypeAdapter(
1838
+ List[CorporateActionAnnouncement],
1839
+ ).validate_python(response)
1840
+
1841
+ def get_corporate_announcement_by_id(
1842
+ self, corporate_announcment_id: Union[UUID, str]
1843
+ ) -> Union[CorporateActionAnnouncement, RawData]:
1844
+ """
1845
+ Returns a specific corporate action announcement.
1846
+ Args:
1847
+ corporate_announcment_id: The id of the desired corporate action announcement
1848
+ Returns:
1849
+ CorporateActionAnnouncement: The corporate action queried.
1850
+ """
1851
+ corporate_announcment_id = validate_uuid_id_param(
1852
+ corporate_announcment_id, "corporate_announcment_id"
1853
+ )
1854
+
1855
+ response = self.get(
1856
+ f"/corporate_actions/announcements/{corporate_announcment_id}"
1857
+ )
1858
+
1859
+ if self._use_raw_data:
1860
+ return response
1861
+
1862
+ return CorporateActionAnnouncement(**response)
1863
+
1864
+ # ############################## EVENTS ################################# #
1865
+
1866
+ def get_account_status_events(
1867
+ self, filter: Optional[GetEventsRequest] = None
1868
+ ) -> Iterator:
1869
+ """
1870
+ Subscribes to SSE stream for account status events.
1871
+
1872
+ Args:
1873
+ filter: The arguments for filtering the events stream.
1874
+
1875
+ Returns:
1876
+ Iterator: Yields events as they arrive
1877
+ """
1878
+
1879
+ params = {}
1880
+
1881
+ if filter:
1882
+ params = filter.to_request_fields()
1883
+
1884
+ url = self._base_url + "/" + self._api_version + "/events/accounts/status"
1885
+
1886
+ response = self._session.get(
1887
+ url=url,
1888
+ params=params,
1889
+ stream=True,
1890
+ headers=self._get_sse_headers(),
1891
+ )
1892
+
1893
+ client = sseclient.SSEClient(response)
1894
+
1895
+ for event in client.events():
1896
+ yield event.data
1897
+
1898
+ def get_trade_events(self, filter: Optional[GetEventsRequest] = None) -> Iterator:
1899
+ """
1900
+ Subscribes to SSE stream for trade events.
1901
+
1902
+ Args:
1903
+ filter: The arguments for filtering the events stream.
1904
+
1905
+ Returns:
1906
+ Iterator: Yields events as they arrive
1907
+ """
1908
+ params = {}
1909
+
1910
+ if filter:
1911
+ params = filter.to_request_fields()
1912
+
1913
+ url = self._base_url + "/" + self._api_version + "/events/trades"
1914
+
1915
+ response = self._session.get(
1916
+ url=url,
1917
+ params=params,
1918
+ stream=True,
1919
+ headers=self._get_sse_headers(),
1920
+ )
1921
+
1922
+ client = sseclient.SSEClient(response)
1923
+
1924
+ for event in client.events():
1925
+ yield event.data
1926
+
1927
+ def get_journal_events(self, filter: Optional[GetEventsRequest] = None) -> Iterator:
1928
+ """
1929
+ Subscribes to SSE stream for journal status events.
1930
+
1931
+ Args:
1932
+ filter: The arguments for filtering the events stream.
1933
+
1934
+ Returns:
1935
+ Iterator: Yields events as they arrive
1936
+ """
1937
+ params = {}
1938
+
1939
+ if filter:
1940
+ params = filter.to_request_fields()
1941
+
1942
+ url = self._base_url + "/" + self._api_version + "/events/journals/status"
1943
+
1944
+ response = self._session.get(
1945
+ url=url,
1946
+ params=params,
1947
+ stream=True,
1948
+ headers=self._get_sse_headers(),
1949
+ )
1950
+
1951
+ client = sseclient.SSEClient(response)
1952
+
1953
+ for event in client.events():
1954
+ yield event.data
1955
+
1956
+ def get_transfer_events(
1957
+ self, filter: Optional[GetEventsRequest] = None
1958
+ ) -> Iterator:
1959
+ """
1960
+ Subscribes to SSE stream for transfer status events.
1961
+
1962
+ Args:
1963
+ filter: The arguments for filtering the events stream.
1964
+
1965
+ Returns:
1966
+ Iterator: Yields events as they arrive
1967
+ """
1968
+ params = {}
1969
+
1970
+ if filter:
1971
+ params = filter.to_request_fields()
1972
+
1973
+ url = self._base_url + "/" + self._api_version + "/events/transfers/status"
1974
+
1975
+ response = self._session.get(
1976
+ url=url,
1977
+ params=params,
1978
+ stream=True,
1979
+ headers=self._get_sse_headers(),
1980
+ )
1981
+
1982
+ client = sseclient.SSEClient(response)
1983
+
1984
+ for event in client.events():
1985
+ yield event.data
1986
+
1987
+ def get_non_trading_activity_events(
1988
+ self, filter: Optional[GetEventsRequest] = None
1989
+ ) -> Iterator:
1990
+ """
1991
+ Subscribes to SSE stream for non trading activity events.
1992
+
1993
+ Args:
1994
+ filter: The arguments for filtering the events stream.
1995
+
1996
+ Returns:
1997
+ Iterator: Yields events as they arrive
1998
+ """
1999
+ params = {}
2000
+
2001
+ if filter:
2002
+ params = filter.to_request_fields()
2003
+
2004
+ url = self._base_url + "/" + self._api_version + "/events/nta"
2005
+
2006
+ response = self._session.get(
2007
+ url=url,
2008
+ params=params,
2009
+ stream=True,
2010
+ headers=self._get_sse_headers(),
2011
+ )
2012
+
2013
+ client = sseclient.SSEClient(response)
2014
+
2015
+ for event in client.events():
2016
+ yield event.data
2017
+
2018
+ def _get_sse_headers(self) -> dict:
2019
+ headers = self._get_default_headers()
2020
+
2021
+ headers["Connection"] = "keep-alive"
2022
+ headers["Cache-Control"] = "no-cache"
2023
+ headers["Content-Type"] = "text/event-stream"
2024
+ headers["Accept"] = "text/event-stream"
2025
+
2026
+ return headers
2027
+
2028
+ # ############################## REBALANCING ################################# #
2029
+
2030
+ def create_portfolio(
2031
+ self, portfolio_request: CreatePortfolioRequest
2032
+ ) -> Union[Portfolio, RawData]:
2033
+ """
2034
+ Create a new portfolio.
2035
+
2036
+ ref. https://docs.alpaca.markets/reference/post-v1-rebalancing-portfolios
2037
+
2038
+ Args:
2039
+ portfolio_request (CreatePortfolioRequest): The details required to create a new portfolio.
2040
+
2041
+ Returns:
2042
+ Portfolio: Newly created portfolio.
2043
+ """
2044
+
2045
+ response = self.post(
2046
+ "/rebalancing/portfolios", data=portfolio_request.to_request_fields()
2047
+ )
2048
+
2049
+ if self._use_raw_data:
2050
+ return response
2051
+
2052
+ return Portfolio(**response)
2053
+
2054
+ def get_all_portfolios(
2055
+ self,
2056
+ filter: Optional[GetPortfoliosRequest] = None,
2057
+ ) -> Union[List[Portfolio], List[RawData]]:
2058
+ """
2059
+ Retrieves all portfolios based on the filter provided.
2060
+
2061
+ ref. https://docs.alpaca.markets/reference/get-v1-rebalancing-portfolios
2062
+
2063
+ Args:
2064
+ filter (Optional[GetPortfoliosRequest]): Filter criteria to narrow down portfolio list.
2065
+
2066
+ Returns:
2067
+ List[Portfolio]: List of portfolios.
2068
+ """
2069
+
2070
+ response = self.get(
2071
+ "/rebalancing/portfolios", filter.to_request_fields() if filter else {}
2072
+ )
2073
+
2074
+ if self._use_raw_data:
2075
+ return response
2076
+
2077
+ return TypeAdapter(
2078
+ List[Portfolio],
2079
+ ).validate_python(response)
2080
+
2081
+ def get_portfolio_by_id(
2082
+ self, portfolio_id: Union[UUID, str]
2083
+ ) -> Union[Portfolio, RawData]:
2084
+ """
2085
+ Retrieves a specific portfolio using its ID.
2086
+
2087
+ Args:
2088
+ portfolio_id (Union[UUID, str]): The ID of the desired portfolio.
2089
+
2090
+ Returns:
2091
+ Portfolio: The portfolio queried.
2092
+ """
2093
+
2094
+ response = self.get(f"/rebalancing/portfolios/{portfolio_id}")
2095
+
2096
+ if self._use_raw_data:
2097
+ return response
2098
+
2099
+ return Portfolio(**response)
2100
+
2101
+ def update_portfolio_by_id(
2102
+ self,
2103
+ portfolio_id: Union[UUID, str],
2104
+ update_request: UpdatePortfolioRequest,
2105
+ ) -> Union[Portfolio, RawData]:
2106
+ """
2107
+ Updates a portfolio by ID.
2108
+ If weights or conditions are changed, all subscribed accounts will be evaluated for rebalancing at the next opportunity (normal market hours).
2109
+ If a cooldown is active on the portfolio, the rebalancing will occur after the cooldown expired.
2110
+
2111
+ ref. https://docs.alpaca.markets/reference/patch-v1-rebalancing-portfolios-portfolio_id-1
2112
+
2113
+ Args:
2114
+ portfolio_id (Union[UUID, str]): The ID of the portfolio to be updated.
2115
+ update_request: The details to be updated for the portfolio.
2116
+
2117
+ Returns:
2118
+ Portfolio: Updated portfolio.
2119
+ """
2120
+ portfolio_id = validate_uuid_id_param(portfolio_id)
2121
+
2122
+ response = self.patch(
2123
+ f"/rebalancing/portfolios/{portfolio_id}",
2124
+ data=update_request.to_request_fields(),
2125
+ )
2126
+
2127
+ if self._use_raw_data:
2128
+ return response
2129
+
2130
+ return Portfolio(**response)
2131
+
2132
+ def inactivate_portfolio_by_id(self, portfolio_id: Union[UUID, str]) -> None:
2133
+ """
2134
+ Sets a portfolio to “inactive”, so it can be filtered out of the list request.
2135
+ Only permitted if there are no active subscriptions to this portfolio and this portfolio is not a listed in the weights of any active portfolios.
2136
+ Inactive portfolios cannot be linked in new subscriptions or added as weights to new portfolios.
2137
+
2138
+ ref. https://docs.alpaca.markets/reference/delete-v1-rebalancing-portfolios-portfolio_id-1
2139
+
2140
+ Args:
2141
+ portfolio_id (Union[UUID, str]): The ID of the portfolio to be inactivated.
2142
+ """
2143
+ portfolio_id = validate_uuid_id_param(portfolio_id)
2144
+
2145
+ self.delete(
2146
+ f"/rebalancing/portfolios/{portfolio_id}",
2147
+ )
2148
+
2149
+ def create_subscription(
2150
+ self, subscription_request: CreateSubscriptionRequest
2151
+ ) -> Union[Subscription, RawData]:
2152
+ """
2153
+ Create a new subscription.
2154
+
2155
+ Args:
2156
+ subscription_request (CreateSubscriptionRequest): The details required to create a new subscription.
2157
+
2158
+ Returns:
2159
+ Subscription: Newly created subscription.
2160
+ """
2161
+
2162
+ response = self.post(
2163
+ "/rebalancing/subscriptions", data=subscription_request.to_request_fields()
2164
+ )
2165
+
2166
+ if self._use_raw_data:
2167
+ return response
2168
+
2169
+ return Subscription(**response)
2170
+
2171
+ def get_all_subscriptions(
2172
+ self,
2173
+ filter: Optional[GetSubscriptionsRequest] = None,
2174
+ max_items_limit: Optional[int] = None,
2175
+ handle_pagination: Optional[PaginationType] = None,
2176
+ ) -> Union[List[Subscription], List[RawData]]:
2177
+ """
2178
+ Retrieves all subscriptions based on the filter provided.
2179
+
2180
+ ref. https://docs.alpaca.markets/reference/get-v1-rebalancing-subscriptions-1
2181
+
2182
+ Args:
2183
+ filter (Optional[GetSubscriptionsRequest]): Filter criteria to narrow down subscription list.
2184
+ max_items_limit (Optional[int]): A maximum number of items to return over all for when handle_pagination is
2185
+ of type `PaginationType.FULL`. Ignored otherwise.
2186
+ handle_pagination (Optional[PaginationType]): What kind of pagination you want. If None then defaults to
2187
+ `PaginationType.FULL`.
2188
+
2189
+ Returns:
2190
+ List[Subscription]: List of subscriptions.
2191
+ """
2192
+ handle_pagination = BrokerClient._validate_pagination(
2193
+ max_items_limit, handle_pagination
2194
+ )
2195
+
2196
+ subscriptions_iterator = self._iterate_over_pages(
2197
+ endpoint="/rebalancing/subscriptions",
2198
+ params=filter.to_request_fields() if filter else {},
2199
+ response_field="subscriptions",
2200
+ base_model_type=Subscription,
2201
+ max_items_limit=max_items_limit,
2202
+ )
2203
+
2204
+ return BrokerClient._return_paginated_result(
2205
+ subscriptions_iterator, handle_pagination
2206
+ )
2207
+
2208
+ def get_subscription_by_id(
2209
+ self, subscription_id: Union[UUID, str]
2210
+ ) -> Union[Subscription, RawData]:
2211
+ """
2212
+ Get a subscription by its ID.
2213
+
2214
+ Args:
2215
+ subscription_id (Union[UUID, str]): The ID of the desired subscription.
2216
+
2217
+ Returns:
2218
+ Subscription: The subscription queried.
2219
+ """
2220
+ subscription_id = validate_uuid_id_param(subscription_id)
2221
+
2222
+ response = self.get(f"/rebalancing/subscriptions/{subscription_id}")
2223
+
2224
+ if self._use_raw_data:
2225
+ return response
2226
+
2227
+ return Subscription(**response)
2228
+
2229
+ def unsubscribe_account(self, subscription_id: Union[UUID, str]) -> None:
2230
+ """
2231
+ Deletes the subscription which stops the rebalancing of an account.
2232
+
2233
+ Args:
2234
+ subscription_id (Union[UUID, str]): The ID of the subscription to be removed.
2235
+ """
2236
+ subscription_id = validate_uuid_id_param(subscription_id)
2237
+
2238
+ self.delete(
2239
+ f"/rebalancing/subscriptions/{subscription_id}",
2240
+ )
2241
+
2242
+ def create_manual_run(
2243
+ self, rebalancing_run_request: CreateRunRequest
2244
+ ) -> Union[RebalancingRun, RawData]:
2245
+ """
2246
+ Create a new manual rebalancing run.
2247
+
2248
+ Args:
2249
+ rebalancing_run_request: The details required to create a new rebalancing run.
2250
+
2251
+ Returns:
2252
+ RebalancingRun: The rebalancing run initiated.
2253
+ """
2254
+
2255
+ response = self.post(
2256
+ "/rebalancing/runs", data=rebalancing_run_request.to_request_fields()
2257
+ )
2258
+
2259
+ if self._use_raw_data:
2260
+ return response
2261
+
2262
+ return RebalancingRun(**response)
2263
+
2264
+ def get_all_runs(
2265
+ self,
2266
+ filter: Optional[GetRunsRequest] = None,
2267
+ max_items_limit: Optional[int] = None,
2268
+ handle_pagination: Optional[PaginationType] = None,
2269
+ ) -> Union[List[RebalancingRun], List[RawData]]:
2270
+ """
2271
+ Get all runs.
2272
+
2273
+ Args:
2274
+ filter (Optional[GetRunsRequest]): Filter criteria to narrow down run list.
2275
+ max_items_limit (Optional[int]): A maximum number of items to return over all for when handle_pagination is
2276
+ of type `PaginationType.FULL`. Ignored otherwise.
2277
+ handle_pagination (Optional[PaginationType]): What kind of pagination you want. If None then defaults to
2278
+ `PaginationType.FULL`.
2279
+
2280
+ Returns:
2281
+ List[RebalancingRun]: List of rebalancing runs.
2282
+ """
2283
+ handle_pagination = BrokerClient._validate_pagination(
2284
+ max_items_limit, handle_pagination
2285
+ )
2286
+
2287
+ runs_iterator = self._iterate_over_pages(
2288
+ endpoint="/rebalancing/runs",
2289
+ params=filter.to_request_fields() if filter else {},
2290
+ response_field="runs",
2291
+ base_model_type=RebalancingRun,
2292
+ max_items_limit=max_items_limit,
2293
+ )
2294
+
2295
+ return BrokerClient._return_paginated_result(runs_iterator, handle_pagination)
2296
+
2297
+ def get_run_by_id(self, run_id: Union[UUID, str]) -> Union[RebalancingRun, RawData]:
2298
+ """
2299
+ Get a run by its ID.
2300
+
2301
+ Args:
2302
+ run_id (Union[UUID, str]): The ID of the desired rebalancing run.
2303
+
2304
+ Returns:
2305
+ RebalancingRun: The rebalancing run queried.
2306
+ """
2307
+ run_id = validate_uuid_id_param(run_id)
2308
+
2309
+ response = self.get(f"/rebalancing/runs/{run_id}")
2310
+
2311
+ if self._use_raw_data:
2312
+ return response
2313
+
2314
+ return RebalancingRun(**response)
2315
+
2316
+ def cancel_run_by_id(self, run_id: Union[UUID, str]) -> None:
2317
+ """
2318
+ Cancels a run.
2319
+
2320
+ Only runs within certain statuses (QUEUED, CANCELED, SELLS_IN_PROGRESS, BUYS_IN_PROGRESS) are cancelable.
2321
+ If this endpoint is called after orders have been submitted, we’ll attempt to cancel the orders.
2322
+
2323
+ Args:
2324
+ run_id (Union[UUID, str]): The ID of the desired rebalancing run.
2325
+ """
2326
+ run_id = validate_uuid_id_param(run_id)
2327
+
2328
+ self.delete(f"/rebalancing/runs/{run_id}")
2329
+
2330
+ def exercise_options_position_for_account_by_id(
2331
+ self,
2332
+ symbol_or_contract_id: Union[UUID, str],
2333
+ account_id: Union[UUID, str],
2334
+ commission: Optional[float] = None,
2335
+ ) -> None:
2336
+ """
2337
+ This endpoint enables the correspondent to exercise a held option contract for an account, converting it into the underlying asset based on the specified terms.
2338
+ All available held shares of this option contract will be exercised.
2339
+ By default, Alpaca will automatically exercise in-the-money (ITM) contracts at expiry.
2340
+ Exercise requests will be processed immediately once received. Exercise requests submitted outside market hours will be rejected.
2341
+ To cancel an exercise request or to submit a Do-not-exercise (DNE) instruction, please contact our support team.
2342
+
2343
+ Args:
2344
+ account_id (Union[UUID, str]): The Account id you wish to retrieve CIPInfo for
2345
+ symbol_or_contract_id (Union[UUID, str]): Option contract symbol or ID.
2346
+ commission (Optional[float]): The dollar value commission you want to charge the end user.
2347
+
2348
+ Returns:
2349
+ None
2350
+ """
2351
+
2352
+ account_id = validate_uuid_id_param(account_id)
2353
+ symbol_or_contract_id = validate_symbol_or_contract_id(symbol_or_contract_id)
2354
+ req = CreateOptionExerciseRequest(commission=commission)
2355
+
2356
+ params = req.to_request_fields()
2357
+ self.post(
2358
+ f"/trading/accounts/{account_id}/positions/{symbol_or_contract_id}/exercise",
2359
+ params,
2360
+ )