etrade-python 1.0.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 (49) hide show
  1. etrade_python/__init__.py +264 -0
  2. etrade_python/_dates.py +152 -0
  3. etrade_python/_version.py +3 -0
  4. etrade_python/accounts/__init__.py +31 -0
  5. etrade_python/accounts/models.py +204 -0
  6. etrade_python/accounts/service.py +136 -0
  7. etrade_python/alerts/__init__.py +23 -0
  8. etrade_python/alerts/models.py +180 -0
  9. etrade_python/alerts/service.py +128 -0
  10. etrade_python/auth/__init__.py +31 -0
  11. etrade_python/auth/credentials.py +85 -0
  12. etrade_python/auth/oauth.py +268 -0
  13. etrade_python/auth/session.py +154 -0
  14. etrade_python/auth/stores/__init__.py +7 -0
  15. etrade_python/auth/stores/base.py +18 -0
  16. etrade_python/auth/stores/keyring.py +49 -0
  17. etrade_python/auth/stores/memory.py +19 -0
  18. etrade_python/cli/__init__.py +1 -0
  19. etrade_python/cli/app.py +1623 -0
  20. etrade_python/client.py +84 -0
  21. etrade_python/config.py +43 -0
  22. etrade_python/enums.py +19 -0
  23. etrade_python/exceptions.py +68 -0
  24. etrade_python/market/__init__.py +59 -0
  25. etrade_python/market/models.py +811 -0
  26. etrade_python/market/service.py +168 -0
  27. etrade_python/orders/__init__.py +107 -0
  28. etrade_python/orders/builders.py +617 -0
  29. etrade_python/orders/models.py +1009 -0
  30. etrade_python/orders/service.py +259 -0
  31. etrade_python/orders/values.py +67 -0
  32. etrade_python/portfolio/__init__.py +35 -0
  33. etrade_python/portfolio/models.py +429 -0
  34. etrade_python/portfolio/service.py +99 -0
  35. etrade_python/py.typed +0 -0
  36. etrade_python/transactions/__init__.py +27 -0
  37. etrade_python/transactions/models.py +201 -0
  38. etrade_python/transactions/service.py +138 -0
  39. etrade_python/transport/__init__.py +7 -0
  40. etrade_python/transport/auth.py +17 -0
  41. etrade_python/transport/http.py +240 -0
  42. etrade_python/transport/logging.py +38 -0
  43. etrade_python/transport/response.py +243 -0
  44. etrade_python/transport/retry.py +75 -0
  45. etrade_python-1.0.0.dist-info/METADATA +513 -0
  46. etrade_python-1.0.0.dist-info/RECORD +49 -0
  47. etrade_python-1.0.0.dist-info/WHEEL +4 -0
  48. etrade_python-1.0.0.dist-info/entry_points.txt +2 -0
  49. etrade_python-1.0.0.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,264 @@
1
+ """Unofficial async E*TRADE client."""
2
+
3
+ from etrade_python._version import __version__ as __version__
4
+ from etrade_python.accounts import (
5
+ Account,
6
+ AccountBalanceRequest,
7
+ AccountBalanceResponse,
8
+ AccountListResponse,
9
+ AccountsService,
10
+ )
11
+ from etrade_python.alerts import (
12
+ Alert,
13
+ AlertDetailsRequest,
14
+ AlertDetailsResponse,
15
+ AlertsRequest,
16
+ AlertsResponse,
17
+ AlertsService,
18
+ DeleteAlertsResponse,
19
+ FailedAlerts,
20
+ )
21
+ from etrade_python.auth import (
22
+ AuthorizationUrl,
23
+ CredentialStore,
24
+ ETradeCredentials,
25
+ KeyringCredentialStore,
26
+ MemoryCredentialStore,
27
+ OAuthClient,
28
+ RenewalResult,
29
+ RequestToken,
30
+ RevocationResult,
31
+ SessionAuthenticator,
32
+ SessionManager,
33
+ TokenStatus,
34
+ )
35
+ from etrade_python.client import ETradeClient
36
+ from etrade_python.config import ETradeSettings
37
+ from etrade_python.enums import Environment
38
+ from etrade_python.exceptions import (
39
+ AuthenticationRequired,
40
+ AuthorizationExpired,
41
+ ETradeApiError,
42
+ ETradeAuthenticationError,
43
+ ETradeError,
44
+ ETradeHttpAuthenticationError,
45
+ ETradeNotFoundError,
46
+ ETradeOrderError,
47
+ ETradeRateLimitError,
48
+ ETradeResponseError,
49
+ ETradeTransportError,
50
+ ETradeUnavailableError,
51
+ ETradeValidationError,
52
+ )
53
+ from etrade_python.market import (
54
+ ExtendedHourQuoteDetails,
55
+ LookupProduct,
56
+ MarketService,
57
+ MutualFundQuoteDetails,
58
+ NetAsset,
59
+ OptionChainPair,
60
+ OptionChainRequest,
61
+ OptionChainResponse,
62
+ OptionContract,
63
+ OptionDeliverable,
64
+ OptionExpiration,
65
+ OptionExpirationsRequest,
66
+ OptionExpirationsResponse,
67
+ OptionGreeks,
68
+ Product,
69
+ ProductLookupRequest,
70
+ ProductLookupResponse,
71
+ Quote,
72
+ QuoteDetails,
73
+ QuoteMessage,
74
+ QuoteMessages,
75
+ QuotesRequest,
76
+ QuotesResponse,
77
+ Redemption,
78
+ RedemptionValues,
79
+ SaleChargeValues,
80
+ )
81
+ from etrade_python.orders import (
82
+ CancelOrderRequest,
83
+ CancelOrderResponse,
84
+ CashBuyingPowerDetails,
85
+ Disclosure,
86
+ DtBuyingPowerDetails,
87
+ Events,
88
+ MarginBuyingPowerDetails,
89
+ Message,
90
+ Messages,
91
+ MFQuantity,
92
+ Order,
93
+ OrderBuilder,
94
+ OrderBuyPowerEffect,
95
+ OrderDetail,
96
+ OrderDetailRequest,
97
+ OrderEvent,
98
+ OrderId,
99
+ OrderInstrument,
100
+ OrderInstrumentRequest,
101
+ OrderLot,
102
+ OrderLots,
103
+ OrderProduct,
104
+ OrderProductId,
105
+ OrderResponseId,
106
+ OrderResponseProduct,
107
+ OrdersRequest,
108
+ OrdersResponse,
109
+ OrdersService,
110
+ PlaceOrderRequest,
111
+ PlaceOrderResponse,
112
+ PortfolioMargin,
113
+ PreviewId,
114
+ PreviewOrderRequest,
115
+ PreviewOrderResponse,
116
+ PreviewResponseId,
117
+ )
118
+ from etrade_python.portfolio import (
119
+ AccountPortfolio,
120
+ CompleteView,
121
+ FundamentalView,
122
+ OptionsWatchView,
123
+ PerformanceView,
124
+ PortfolioProduct,
125
+ PortfolioRequest,
126
+ PortfolioResponse,
127
+ PortfolioService,
128
+ PortfolioTotals,
129
+ Position,
130
+ PositionLot,
131
+ QuickView,
132
+ )
133
+ from etrade_python.transactions import (
134
+ Transaction,
135
+ TransactionDetailsRequest,
136
+ TransactionDetailsResponse,
137
+ TransactionsRequest,
138
+ TransactionsResponse,
139
+ TransactionsService,
140
+ )
141
+
142
+ __all__ = [
143
+ "PreviewResponseId",
144
+ "OrderResponseId",
145
+ "OrderResponseProduct",
146
+ "QuickView",
147
+ "PortfolioTotals",
148
+ "PortfolioProduct",
149
+ "PerformanceView",
150
+ "OptionsWatchView",
151
+ "FundamentalView",
152
+ "CompleteView",
153
+ "AccountPortfolio",
154
+ "PortfolioMargin",
155
+ "OrderProductId",
156
+ "MarginBuyingPowerDetails",
157
+ "DtBuyingPowerDetails",
158
+ "CashBuyingPowerDetails",
159
+ "SaleChargeValues",
160
+ "RedemptionValues",
161
+ "Redemption",
162
+ "QuoteMessages",
163
+ "QuoteMessage",
164
+ "OptionDeliverable",
165
+ "NetAsset",
166
+ "MutualFundQuoteDetails",
167
+ "ExtendedHourQuoteDetails",
168
+ "Alert",
169
+ "AlertDetailsRequest",
170
+ "AlertDetailsResponse",
171
+ "AlertsRequest",
172
+ "AlertsResponse",
173
+ "AlertsService",
174
+ "DeleteAlertsResponse",
175
+ "FailedAlerts",
176
+ "OrderBuilder",
177
+ "PreviewOrderResponse",
178
+ "PreviewOrderRequest",
179
+ "PreviewId",
180
+ "PlaceOrderResponse",
181
+ "PlaceOrderRequest",
182
+ "OrdersService",
183
+ "OrdersResponse",
184
+ "OrdersRequest",
185
+ "OrderProduct",
186
+ "OrderLots",
187
+ "OrderLot",
188
+ "OrderInstrumentRequest",
189
+ "OrderInstrument",
190
+ "OrderId",
191
+ "OrderEvent",
192
+ "OrderDetailRequest",
193
+ "OrderDetail",
194
+ "OrderBuyPowerEffect",
195
+ "Order",
196
+ "MFQuantity",
197
+ "Messages",
198
+ "Message",
199
+ "Events",
200
+ "Disclosure",
201
+ "CancelOrderResponse",
202
+ "CancelOrderRequest",
203
+ "AuthenticationRequired",
204
+ "Account",
205
+ "AccountBalanceRequest",
206
+ "AccountBalanceResponse",
207
+ "AccountListResponse",
208
+ "AccountsService",
209
+ "AuthorizationUrl",
210
+ "AuthorizationExpired",
211
+ "CredentialStore",
212
+ "ETradeApiError",
213
+ "ETradeAuthenticationError",
214
+ "ETradeClient",
215
+ "ETradeCredentials",
216
+ "ETradeError",
217
+ "ETradeHttpAuthenticationError",
218
+ "ETradeNotFoundError",
219
+ "ETradeOrderError",
220
+ "ETradeRateLimitError",
221
+ "ETradeResponseError",
222
+ "ETradeSettings",
223
+ "ETradeTransportError",
224
+ "ETradeUnavailableError",
225
+ "ETradeValidationError",
226
+ "Environment",
227
+ "KeyringCredentialStore",
228
+ "LookupProduct",
229
+ "MarketService",
230
+ "OptionChainPair",
231
+ "OptionChainRequest",
232
+ "OptionChainResponse",
233
+ "OptionContract",
234
+ "OptionExpiration",
235
+ "OptionExpirationsRequest",
236
+ "OptionExpirationsResponse",
237
+ "OptionGreeks",
238
+ "Product",
239
+ "ProductLookupRequest",
240
+ "ProductLookupResponse",
241
+ "Quote",
242
+ "QuoteDetails",
243
+ "QuotesRequest",
244
+ "QuotesResponse",
245
+ "MemoryCredentialStore",
246
+ "PortfolioRequest",
247
+ "PortfolioResponse",
248
+ "PortfolioService",
249
+ "Position",
250
+ "PositionLot",
251
+ "Transaction",
252
+ "TransactionDetailsRequest",
253
+ "TransactionDetailsResponse",
254
+ "TransactionsRequest",
255
+ "TransactionsResponse",
256
+ "TransactionsService",
257
+ "OAuthClient",
258
+ "RenewalResult",
259
+ "RequestToken",
260
+ "RevocationResult",
261
+ "SessionAuthenticator",
262
+ "SessionManager",
263
+ "TokenStatus",
264
+ ]
@@ -0,0 +1,152 @@
1
+ """Internal helpers for parsing broker date/time values."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from datetime import UTC, date, datetime, time, timedelta, timezone
7
+ from typing import Any
8
+
9
+ # E*TRADE's Getting Started guide specifies US Eastern Time (EST/EDT),
10
+ # but its quote documentation includes a PDT example. Additional abbreviations
11
+ # and numeric offsets improve compatibility; this is not universal timezone support.
12
+ # Abbreviations use their US meanings; explicit labels determine the offset.
13
+ _QUOTE_TIMEZONE_HOURS = {
14
+ "EST": -5,
15
+ "EDT": -4,
16
+ "CST": -6,
17
+ "CDT": -5,
18
+ "MST": -7,
19
+ "MDT": -6,
20
+ "PST": -8,
21
+ "PDT": -7,
22
+ "UTC": 0,
23
+ "GMT": 0,
24
+ }
25
+
26
+
27
+ def parse_broker_datetime(value: Any) -> datetime | None:
28
+ """Parse an E*TRADE date/time value into a timezone-aware datetime.
29
+
30
+ E*TRADE responses commonly encode date/time fields as epoch milliseconds,
31
+ epoch seconds, compact numeric dates, or broker-specific timestamp strings.
32
+ """
33
+ if value is None or value == "":
34
+ return None
35
+
36
+ if isinstance(value, datetime):
37
+ return _ensure_timezone(value)
38
+
39
+ if isinstance(value, date):
40
+ return datetime.combine(value, time.min, tzinfo=UTC)
41
+
42
+ if isinstance(value, int | float):
43
+ return _parse_numeric_datetime(value)
44
+
45
+ if isinstance(value, str):
46
+ trimmed = value.strip()
47
+ if not trimmed:
48
+ return None
49
+ if _is_numeric_string(trimmed):
50
+ return _parse_numeric_datetime(int(trimmed))
51
+ return _parse_string_datetime(trimmed)
52
+
53
+ return None
54
+
55
+
56
+ def parse_broker_date(value: Any) -> date | None:
57
+ """Parse an E*TRADE date/time value into a date."""
58
+ parsed = parse_broker_datetime(value)
59
+ if parsed is None:
60
+ return None
61
+ return parsed.date()
62
+
63
+
64
+ def _parse_numeric_datetime(value: int | float) -> datetime | None:
65
+ integer = int(value)
66
+ if integer == 0:
67
+ return None
68
+
69
+ digits = str(abs(integer))
70
+ length = len(digits)
71
+
72
+ if length >= 13:
73
+ return datetime.fromtimestamp(integer / 1000, tz=UTC)
74
+
75
+ if length == 8:
76
+ try:
77
+ parsed_date = datetime.strptime(digits, "%Y%m%d").date()
78
+ except ValueError:
79
+ return None
80
+ return datetime.combine(parsed_date, time.min, tzinfo=UTC)
81
+
82
+ if length in {4, 6}:
83
+ time_format = "%H%M%S" if length == 6 else "%H%M"
84
+ try:
85
+ parsed_time = datetime.strptime(digits, time_format).time()
86
+ except ValueError:
87
+ return None
88
+ return datetime.combine(date.today(), parsed_time, tzinfo=UTC)
89
+
90
+ return datetime.fromtimestamp(integer, tz=UTC)
91
+
92
+
93
+ def _parse_string_datetime(value: str) -> datetime | None:
94
+ for candidate in _string_datetime_candidates(value):
95
+ try:
96
+ parsed = datetime.fromisoformat(candidate)
97
+ except ValueError:
98
+ continue
99
+ return _ensure_timezone(parsed)
100
+
101
+ quote_datetime = _parse_quote_datetime(value)
102
+ if quote_datetime is not None:
103
+ return quote_datetime
104
+
105
+ for fmt in ("%Y-%m-%d", "%m/%d/%Y", "%m-%d-%Y"):
106
+ try:
107
+ parsed = datetime.strptime(value, fmt)
108
+ except ValueError:
109
+ continue
110
+ return _ensure_timezone(parsed)
111
+
112
+ return None
113
+
114
+
115
+ def _parse_quote_datetime(value: str) -> datetime | None:
116
+ parts = value.split()
117
+ if len(parts) != 3:
118
+ return None
119
+ try:
120
+ parsed = datetime.strptime(f"{parts[0]} {parts[2]}", "%H:%M:%S %m-%d-%Y")
121
+ except ValueError:
122
+ return None
123
+ label = parts[1]
124
+ if label in _QUOTE_TIMEZONE_HOURS:
125
+ offset = timedelta(hours=_QUOTE_TIMEZONE_HOURS[label])
126
+ else:
127
+ match = re.fullmatch(r"([+-])([0-9]{2}):?([0-9]{2})", label)
128
+ if match is None:
129
+ raise ValueError("Unsupported or invalid quote timestamp timezone")
130
+ hours, minutes = int(match[2]), int(match[3])
131
+ if hours > 23 or minutes > 59:
132
+ raise ValueError("Unsupported or invalid quote timestamp timezone")
133
+ offset = timedelta(hours=hours, minutes=minutes)
134
+ if match[1] == "-":
135
+ offset = -offset
136
+ return parsed.replace(tzinfo=timezone(offset)).astimezone(UTC)
137
+
138
+
139
+ def _string_datetime_candidates(value: str) -> tuple[str, ...]:
140
+ if value.endswith("Z"):
141
+ return (value[:-1] + "+00:00", value)
142
+ return (value,)
143
+
144
+
145
+ def _ensure_timezone(value: datetime) -> datetime:
146
+ if value.tzinfo is None:
147
+ return value.replace(tzinfo=UTC)
148
+ return value.astimezone(UTC)
149
+
150
+
151
+ def _is_numeric_string(value: str) -> bool:
152
+ return value.isdecimal() or (value.startswith("-") and value[1:].isdecimal())
@@ -0,0 +1,3 @@
1
+ """Runtime version shared by the public package and HTTP transport."""
2
+
3
+ __version__ = "1.0.0"
@@ -0,0 +1,31 @@
1
+ """Accounts API service and models."""
2
+
3
+ from etrade_python.accounts.models import (
4
+ Account,
5
+ AccountBalanceRequest,
6
+ AccountBalanceResponse,
7
+ AccountListResponse,
8
+ CashBalance,
9
+ ComputedBalance,
10
+ LendingBalance,
11
+ MarginBalance,
12
+ OpenCalls,
13
+ PortfolioMargin,
14
+ RealTimeValues,
15
+ )
16
+ from etrade_python.accounts.service import AccountsService
17
+
18
+ __all__ = [
19
+ "Account",
20
+ "AccountBalanceRequest",
21
+ "AccountBalanceResponse",
22
+ "AccountListResponse",
23
+ "AccountsService",
24
+ "CashBalance",
25
+ "ComputedBalance",
26
+ "LendingBalance",
27
+ "MarginBalance",
28
+ "OpenCalls",
29
+ "PortfolioMargin",
30
+ "RealTimeValues",
31
+ ]
@@ -0,0 +1,204 @@
1
+ """Typed account request and response models."""
2
+
3
+ from datetime import date
4
+ from decimal import Decimal
5
+ from typing import Any, cast
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
8
+
9
+ from etrade_python._dates import parse_broker_date
10
+
11
+
12
+ class BrokerModel(BaseModel):
13
+ """Base response model that discards unmodeled broker fields."""
14
+
15
+ model_config = ConfigDict(extra="ignore", frozen=True, populate_by_name=True)
16
+
17
+
18
+ class AccountBalanceRequest(BaseModel):
19
+ """Query parameters for the account balance endpoint."""
20
+
21
+ model_config = ConfigDict(frozen=True)
22
+
23
+ inst_type: str = "BROKERAGE"
24
+ account_type: str | None = None
25
+ real_time_nav: bool = False
26
+
27
+ @field_validator("inst_type", "account_type")
28
+ @classmethod
29
+ def nonempty_string(cls, value: str | None) -> str | None:
30
+ if value is not None and not value.strip():
31
+ raise ValueError("A nonempty value is required")
32
+ return value
33
+
34
+ def query_params(self) -> dict[str, str | bool | None]:
35
+ return {
36
+ "instType": self.inst_type,
37
+ "accountType": self.account_type,
38
+ "realTimeNAV": self.real_time_nav,
39
+ }
40
+
41
+
42
+ class Account(BrokerModel):
43
+ inst_no: int | None = Field(default=None, alias="instNo")
44
+ account_id: str = Field(alias="accountId")
45
+ account_id_key: str = Field(alias="accountIdKey")
46
+ account_mode: str | None = Field(default=None, alias="accountMode")
47
+ account_desc: str | None = Field(default=None, alias="accountDesc")
48
+ account_name: str | None = Field(default=None, alias="accountName")
49
+ account_type: str | None = Field(default=None, alias="accountType")
50
+ institution_type: str | None = Field(default=None, alias="institutionType")
51
+ account_status: str | None = Field(default=None, alias="accountStatus")
52
+ closed_date: date | None = Field(default=None, alias="closedDate")
53
+ shareworks_account: bool | None = Field(default=None, alias="shareWorksAccount")
54
+ shareworks_source: str | None = Field(default=None, alias="shareWorksSource")
55
+ fc_managed_mssb_closed_account: bool | None = Field(
56
+ default=None, alias="fcManagedMssbClosedAccount"
57
+ )
58
+
59
+ @field_validator("closed_date", mode="before")
60
+ @classmethod
61
+ def parse_closed_date(cls, value: object) -> date | None:
62
+ return parse_broker_date(value)
63
+
64
+
65
+ def _empty_accounts() -> list[Account]:
66
+ return []
67
+
68
+
69
+ class AccountListResponse(BrokerModel):
70
+ accounts: list[Account] = Field(default_factory=_empty_accounts)
71
+
72
+
73
+ class CashBalance(BrokerModel):
74
+ funds_for_open_orders_cash: Decimal | None = Field(default=None, alias="fundsForOpenOrdersCash")
75
+ money_market_balance: Decimal | None = Field(default=None, alias="moneyMktBalance")
76
+
77
+
78
+ class OpenCalls(BrokerModel):
79
+ min_equity_call: Decimal | None = Field(default=None, alias="minEquityCall")
80
+ fed_call: Decimal | None = Field(default=None, alias="fedCall")
81
+ cash_call: Decimal | None = Field(default=None, alias="cashCall")
82
+ house_call: Decimal | None = Field(default=None, alias="houseCall")
83
+
84
+
85
+ class RealTimeValues(BrokerModel):
86
+ total_account_value: Decimal | None = Field(default=None, alias="totalAccountValue")
87
+ net_market_value: Decimal | None = Field(default=None, alias="netMv")
88
+ net_market_value_long: Decimal | None = Field(default=None, alias="netMvLong")
89
+ net_market_value_short: Decimal | None = Field(default=None, alias="netMvShort")
90
+ total_long_value: Decimal | None = Field(default=None, alias="totalLongValue")
91
+
92
+
93
+ class PortfolioMargin(BrokerModel):
94
+ dt_cash_open_order_reserve: Decimal | None = Field(default=None, alias="dtCashOpenOrderReserve")
95
+ dt_margin_open_order_reserve: Decimal | None = Field(
96
+ default=None, alias="dtMarginOpenOrderReserve"
97
+ )
98
+ liquidating_equity: Decimal | None = Field(default=None, alias="liquidatingEquity")
99
+ house_excess_equity: Decimal | None = Field(default=None, alias="houseExcessEquity")
100
+ total_house_requirement: Decimal | None = Field(default=None, alias="totalHouseRequirement")
101
+ excess_equity_minus_requirement: Decimal | None = Field(
102
+ default=None, alias="excessEquityMinusRequirement"
103
+ )
104
+ total_margin_requirements: Decimal | None = Field(default=None, alias="totalMarginRqmts")
105
+ available_excess_equity: Decimal | None = Field(default=None, alias="availExcessEquity")
106
+ excess_equity: Decimal | None = Field(default=None, alias="excessEquity")
107
+ open_order_reserve: Decimal | None = Field(default=None, alias="openOrderReserve")
108
+ funds_on_hold: Decimal | None = Field(default=None, alias="fundsOnHold")
109
+
110
+
111
+ class ComputedBalance(BrokerModel):
112
+ cash_available_for_investment: Decimal | None = Field(
113
+ default=None, alias="cashAvailableForInvestment"
114
+ )
115
+ cash_available_for_withdrawal: Decimal | None = Field(
116
+ default=None, alias="cashAvailableForWithdrawal"
117
+ )
118
+ total_available_for_withdrawal: Decimal | None = Field(
119
+ default=None, alias="totalAvailableForWithdrawal"
120
+ )
121
+ net_cash: Decimal | None = Field(default=None, alias="netCash")
122
+ cash_balance: Decimal | None = Field(default=None, alias="cashBalance")
123
+ settled_cash_for_investment: Decimal | None = Field(
124
+ default=None, alias="settledCashForInvestment"
125
+ )
126
+ unsettled_cash_for_investment: Decimal | None = Field(
127
+ default=None, alias="unSettledCashForInvestment"
128
+ )
129
+ funds_withheld_from_purchase_power: Decimal | None = Field(
130
+ default=None, alias="fundsWithheldFromPurchasePower"
131
+ )
132
+ funds_withheld_from_withdrawal: Decimal | None = Field(
133
+ default=None, alias="fundsWithheldFromWithdrawal"
134
+ )
135
+ margin_buying_power: Decimal | None = Field(default=None, alias="marginBuyingPower")
136
+ cash_buying_power: Decimal | None = Field(default=None, alias="cashBuyingPower")
137
+ day_trade_margin_buying_power: Decimal | None = Field(default=None, alias="dtMarginBuyingPower")
138
+ day_trade_cash_buying_power: Decimal | None = Field(default=None, alias="dtCashBuyingPower")
139
+ margin_balance: Decimal | None = Field(default=None, alias="marginBalance")
140
+ short_adjust_balance: Decimal | None = Field(default=None, alias="shortAdjustBalance")
141
+ regulation_t_equity: Decimal | None = Field(default=None, alias="regtEquity")
142
+ regulation_t_equity_percent: Decimal | None = Field(default=None, alias="regtEquityPercent")
143
+ account_balance: Decimal | None = Field(default=None, alias="accountBalance")
144
+ open_calls: OpenCalls | None = Field(default=None, alias="openCalls")
145
+ real_time_values: RealTimeValues | None = Field(default=None, alias="realTimeValues")
146
+ portfolio_margin: PortfolioMargin | None = Field(default=None, alias="portfolioMargin")
147
+
148
+
149
+ class LendingBalance(BrokerModel):
150
+ current_balance: Decimal | None = Field(default=None, alias="currentBalance")
151
+ credit_line: Decimal | None = Field(default=None, alias="creditLine")
152
+ outstanding_balance: Decimal | None = Field(default=None, alias="outstandingBalance")
153
+ minimum_payment_due: Decimal | None = Field(default=None, alias="minPaymentDue")
154
+ amount_past_due: Decimal | None = Field(default=None, alias="amountPastDue")
155
+ available_credit: Decimal | None = Field(default=None, alias="availableCredit")
156
+ ytd_interest_paid: Decimal | None = Field(default=None, alias="ytdInterestPaid")
157
+ last_ytd_interest_paid: Decimal | None = Field(default=None, alias="lastYtdInterestPaid")
158
+ payment_due_date: date | None = Field(default=None, alias="paymentDueDate")
159
+ last_payment_received_date: date | None = Field(default=None, alias="lastPaymentReceivedDate")
160
+ payment_received_month_to_date: Decimal | None = Field(default=None, alias="paymentReceivedMtd")
161
+
162
+ @field_validator("payment_due_date", "last_payment_received_date", mode="before")
163
+ @classmethod
164
+ def parse_lending_dates(cls, value: object) -> date | None:
165
+ return parse_broker_date(value)
166
+
167
+
168
+ class MarginBalance(BrokerModel):
169
+ dt_cash_open_order_reserve: Decimal | None = Field(default=None, alias="dtCashOpenOrderReserve")
170
+ dt_margin_open_order_reserve: Decimal | None = Field(
171
+ default=None, alias="dtMarginOpenOrderReserve"
172
+ )
173
+
174
+
175
+ class AccountBalanceResponse(BrokerModel):
176
+ account_id: str = Field(alias="accountId")
177
+ institution_type: str | None = Field(default=None, alias="institutionType")
178
+ as_of_date: date | None = Field(default=None, alias="asOfDate")
179
+ account_type: str | None = Field(default=None, alias="accountType")
180
+ option_level: str | None = Field(default=None, alias="optionLevel")
181
+ account_description: str | None = Field(default=None, alias="accountDescription")
182
+ quote_mode: int | None = Field(default=None, alias="quoteMode")
183
+ day_trader_status: str | None = Field(default=None, alias="dayTraderStatus")
184
+ account_mode: str | None = Field(default=None, alias="accountMode")
185
+ account_desc: str | None = Field(default=None, alias="accountDesc")
186
+ open_calls: list[OpenCalls] = Field(default_factory=lambda: [], alias="openCalls")
187
+ cash: CashBalance | None = Field(default=None, alias="cash")
188
+ margin: MarginBalance | None = Field(default=None, alias="margin")
189
+ lending: LendingBalance | None = Field(default=None, alias="lending")
190
+ computed_balance: ComputedBalance | None = Field(default=None, alias="computedBalance")
191
+
192
+ @field_validator("as_of_date", mode="before")
193
+ @classmethod
194
+ def parse_as_of_date(cls, value: object) -> date | None:
195
+ return parse_broker_date(value)
196
+
197
+ @field_validator("open_calls", mode="before")
198
+ @classmethod
199
+ def normalize_open_calls(cls, value: object) -> object:
200
+ if value is None:
201
+ return []
202
+ if isinstance(value, dict):
203
+ return [cast(dict[str, Any], value)]
204
+ return value