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.
- etrade_python/__init__.py +264 -0
- etrade_python/_dates.py +152 -0
- etrade_python/_version.py +3 -0
- etrade_python/accounts/__init__.py +31 -0
- etrade_python/accounts/models.py +204 -0
- etrade_python/accounts/service.py +136 -0
- etrade_python/alerts/__init__.py +23 -0
- etrade_python/alerts/models.py +180 -0
- etrade_python/alerts/service.py +128 -0
- etrade_python/auth/__init__.py +31 -0
- etrade_python/auth/credentials.py +85 -0
- etrade_python/auth/oauth.py +268 -0
- etrade_python/auth/session.py +154 -0
- etrade_python/auth/stores/__init__.py +7 -0
- etrade_python/auth/stores/base.py +18 -0
- etrade_python/auth/stores/keyring.py +49 -0
- etrade_python/auth/stores/memory.py +19 -0
- etrade_python/cli/__init__.py +1 -0
- etrade_python/cli/app.py +1623 -0
- etrade_python/client.py +84 -0
- etrade_python/config.py +43 -0
- etrade_python/enums.py +19 -0
- etrade_python/exceptions.py +68 -0
- etrade_python/market/__init__.py +59 -0
- etrade_python/market/models.py +811 -0
- etrade_python/market/service.py +168 -0
- etrade_python/orders/__init__.py +107 -0
- etrade_python/orders/builders.py +617 -0
- etrade_python/orders/models.py +1009 -0
- etrade_python/orders/service.py +259 -0
- etrade_python/orders/values.py +67 -0
- etrade_python/portfolio/__init__.py +35 -0
- etrade_python/portfolio/models.py +429 -0
- etrade_python/portfolio/service.py +99 -0
- etrade_python/py.typed +0 -0
- etrade_python/transactions/__init__.py +27 -0
- etrade_python/transactions/models.py +201 -0
- etrade_python/transactions/service.py +138 -0
- etrade_python/transport/__init__.py +7 -0
- etrade_python/transport/auth.py +17 -0
- etrade_python/transport/http.py +240 -0
- etrade_python/transport/logging.py +38 -0
- etrade_python/transport/response.py +243 -0
- etrade_python/transport/retry.py +75 -0
- etrade_python-1.0.0.dist-info/METADATA +513 -0
- etrade_python-1.0.0.dist-info/RECORD +49 -0
- etrade_python-1.0.0.dist-info/WHEEL +4 -0
- etrade_python-1.0.0.dist-info/entry_points.txt +2 -0
- 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
|
+
]
|
etrade_python/_dates.py
ADDED
|
@@ -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,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
|