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,429 @@
|
|
|
1
|
+
"""Typed portfolio request and response models."""
|
|
2
|
+
|
|
3
|
+
from datetime import date, datetime
|
|
4
|
+
from decimal import Decimal
|
|
5
|
+
from typing import Any, cast
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
8
|
+
|
|
9
|
+
from etrade_python._dates import parse_broker_date, parse_broker_datetime
|
|
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
|
+
def _bool_query(value: bool | None) -> str | None:
|
|
19
|
+
if value is None:
|
|
20
|
+
return None
|
|
21
|
+
return "true" if value else "false"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PortfolioRequest(BaseModel):
|
|
25
|
+
"""Query parameters for the portfolio endpoint."""
|
|
26
|
+
|
|
27
|
+
model_config = ConfigDict(frozen=True)
|
|
28
|
+
|
|
29
|
+
count: int | None = Field(default=None, ge=1)
|
|
30
|
+
sort_by: str | None = None
|
|
31
|
+
sort_order: str | None = None
|
|
32
|
+
page_number: int | None = Field(default=None, ge=1)
|
|
33
|
+
market_session: str | None = None
|
|
34
|
+
totals_required: bool | None = None
|
|
35
|
+
lots_required: bool | None = None
|
|
36
|
+
view: str | None = None
|
|
37
|
+
|
|
38
|
+
@field_validator("sort_by", "sort_order", "market_session", "view")
|
|
39
|
+
@classmethod
|
|
40
|
+
def nonempty_string(cls, value: str | None) -> str | None:
|
|
41
|
+
if value is not None and not value.strip():
|
|
42
|
+
raise ValueError("A nonempty value is required")
|
|
43
|
+
return value
|
|
44
|
+
|
|
45
|
+
def query_params(self) -> dict[str, str | int | None]:
|
|
46
|
+
return {
|
|
47
|
+
"count": self.count,
|
|
48
|
+
"sortBy": self.sort_by,
|
|
49
|
+
"sortOrder": self.sort_order,
|
|
50
|
+
"pageNumber": self.page_number,
|
|
51
|
+
"marketSession": self.market_session,
|
|
52
|
+
"totalsRequired": _bool_query(self.totals_required),
|
|
53
|
+
"lotsRequired": _bool_query(self.lots_required),
|
|
54
|
+
"view": self.view,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ProductId(BrokerModel):
|
|
59
|
+
symbol: str | None = None
|
|
60
|
+
type_code: str | None = Field(default=None, alias="typeCode")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class PortfolioProduct(BrokerModel):
|
|
64
|
+
symbol: str | None = None
|
|
65
|
+
security_type: str | None = Field(default=None, alias="securityType")
|
|
66
|
+
security_sub_type: str | None = Field(default=None, alias="securitySubType")
|
|
67
|
+
call_put: str | None = Field(default=None, alias="callPut")
|
|
68
|
+
expiry_year: int | None = Field(default=None, alias="expiryYear")
|
|
69
|
+
expiry_month: int | None = Field(default=None, alias="expiryMonth")
|
|
70
|
+
expiry_day: int | None = Field(default=None, alias="expiryDay")
|
|
71
|
+
strike_price: Decimal | None = Field(default=None, alias="strikePrice")
|
|
72
|
+
expiry_type: str | None = Field(default=None, alias="expiryType")
|
|
73
|
+
product_id: ProductId | None = Field(default=None, alias="productId")
|
|
74
|
+
|
|
75
|
+
@model_validator(mode="before")
|
|
76
|
+
@classmethod
|
|
77
|
+
def normalize_product_id(cls, value: object) -> object:
|
|
78
|
+
if not isinstance(value, dict):
|
|
79
|
+
return value
|
|
80
|
+
data = dict(cast(dict[str, Any], value))
|
|
81
|
+
if "ProductId" in data and "productId" not in data:
|
|
82
|
+
data["productId"] = data["ProductId"]
|
|
83
|
+
data.pop("ProductId", None)
|
|
84
|
+
return data
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class QuickView(BrokerModel):
|
|
88
|
+
last_trade: Decimal | None = Field(default=None, alias="lastTrade")
|
|
89
|
+
last_trade_time: datetime | None = Field(default=None, alias="lastTradeTime")
|
|
90
|
+
change: Decimal | None = None
|
|
91
|
+
change_pct: Decimal | None = Field(default=None, alias="changePct")
|
|
92
|
+
volume: int | None = None
|
|
93
|
+
quote_status: str | None = Field(default=None, alias="quoteStatus")
|
|
94
|
+
seven_day_current_yield: Decimal | None = Field(default=None, alias="sevenDayCurrentYield")
|
|
95
|
+
annual_total_return: Decimal | None = Field(default=None, alias="annualTotalReturn")
|
|
96
|
+
weighted_average_maturity: Decimal | None = Field(default=None, alias="weightedAverageMaturity")
|
|
97
|
+
|
|
98
|
+
@field_validator("last_trade_time", mode="before")
|
|
99
|
+
@classmethod
|
|
100
|
+
def parse_last_trade_time(cls, value: object) -> datetime | None:
|
|
101
|
+
return parse_broker_datetime(value)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class PerformanceView(BrokerModel):
|
|
105
|
+
change: Decimal | None = None
|
|
106
|
+
change_pct: Decimal | None = Field(default=None, alias="changePct")
|
|
107
|
+
last_trade: Decimal | None = Field(default=None, alias="lastTrade")
|
|
108
|
+
days_gain: Decimal | None = Field(default=None, alias="daysGain")
|
|
109
|
+
total_gain: Decimal | None = Field(default=None, alias="totalGain")
|
|
110
|
+
total_gain_pct: Decimal | None = Field(default=None, alias="totalGainPct")
|
|
111
|
+
market_value: Decimal | None = Field(default=None, alias="marketValue")
|
|
112
|
+
quote_status: str | None = Field(default=None, alias="quoteStatus")
|
|
113
|
+
last_trade_time: datetime | None = Field(default=None, alias="lastTradeTime")
|
|
114
|
+
|
|
115
|
+
@field_validator("last_trade_time", mode="before")
|
|
116
|
+
@classmethod
|
|
117
|
+
def parse_last_trade_time(cls, value: object) -> datetime | None:
|
|
118
|
+
return parse_broker_datetime(value)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class FundamentalView(BrokerModel):
|
|
122
|
+
last_trade: Decimal | None = Field(default=None, alias="lastTrade")
|
|
123
|
+
last_trade_time: datetime | None = Field(default=None, alias="lastTradeTime")
|
|
124
|
+
change: Decimal | None = None
|
|
125
|
+
change_pct: Decimal | None = Field(default=None, alias="changePct")
|
|
126
|
+
pe_ratio: Decimal | None = Field(default=None, alias="peRatio")
|
|
127
|
+
eps: Decimal | None = None
|
|
128
|
+
dividend: Decimal | None = None
|
|
129
|
+
div_yield: Decimal | None = Field(default=None, alias="divYield")
|
|
130
|
+
market_cap: Decimal | None = Field(default=None, alias="marketCap")
|
|
131
|
+
week52_range: str | None = Field(default=None, alias="week52Range")
|
|
132
|
+
quote_status: str | None = Field(default=None, alias="quoteStatus")
|
|
133
|
+
|
|
134
|
+
@field_validator("last_trade_time", mode="before")
|
|
135
|
+
@classmethod
|
|
136
|
+
def parse_last_trade_time(cls, value: object) -> datetime | None:
|
|
137
|
+
return parse_broker_datetime(value)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class OptionsWatchView(BrokerModel):
|
|
141
|
+
base_symbol_and_price: str | None = Field(default=None, alias="baseSymbolAndPrice")
|
|
142
|
+
premium: Decimal | None = None
|
|
143
|
+
last_trade: Decimal | None = Field(default=None, alias="lastTrade")
|
|
144
|
+
bid: Decimal | None = None
|
|
145
|
+
ask: Decimal | None = None
|
|
146
|
+
quote_status: str | None = Field(default=None, alias="quoteStatus")
|
|
147
|
+
last_trade_time: datetime | None = Field(default=None, alias="lastTradeTime")
|
|
148
|
+
|
|
149
|
+
@field_validator("last_trade_time", mode="before")
|
|
150
|
+
@classmethod
|
|
151
|
+
def parse_last_trade_time(cls, value: object) -> datetime | None:
|
|
152
|
+
return parse_broker_datetime(value)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class CompleteView(BrokerModel):
|
|
156
|
+
price_adjusted_flag: bool | None = Field(default=None, alias="priceAdjustedFlag")
|
|
157
|
+
price: Decimal | None = None
|
|
158
|
+
adj_price: Decimal | None = Field(default=None, alias="adjPrice")
|
|
159
|
+
change: Decimal | None = None
|
|
160
|
+
change_pct: Decimal | None = Field(default=None, alias="changePct")
|
|
161
|
+
prev_close: Decimal | None = Field(default=None, alias="prevClose")
|
|
162
|
+
adj_prev_close: Decimal | None = Field(default=None, alias="adjPrevClose")
|
|
163
|
+
volume: Decimal | None = None
|
|
164
|
+
last_trade: Decimal | None = Field(default=None, alias="lastTrade")
|
|
165
|
+
last_trade_time: datetime | None = Field(default=None, alias="lastTradeTime")
|
|
166
|
+
symbol_description: str | None = Field(default=None, alias="symbolDescription")
|
|
167
|
+
bid: Decimal | None = None
|
|
168
|
+
ask: Decimal | None = None
|
|
169
|
+
quote_status: str | None = Field(default=None, alias="quoteStatus")
|
|
170
|
+
div_pay_date: date | None = Field(default=None, alias="divPayDate")
|
|
171
|
+
ex_dividend_date: date | None = Field(default=None, alias="exDividendDate")
|
|
172
|
+
adj_last_trade: Decimal | None = Field(default=None, alias="adjLastTrade")
|
|
173
|
+
perform1_month: Decimal | None = Field(default=None, alias="perform1Month")
|
|
174
|
+
perform3_month: Decimal | None = Field(default=None, alias="perform3Month")
|
|
175
|
+
perform6_month: Decimal | None = Field(default=None, alias="perform6Month")
|
|
176
|
+
perform12_month: Decimal | None = Field(default=None, alias="perform12Month")
|
|
177
|
+
prev_day_volume: int | None = Field(default=None, alias="prevDayVolume")
|
|
178
|
+
ten_day_volume: int | None = Field(default=None, alias="tenDayVolume")
|
|
179
|
+
beta: Decimal | None = None
|
|
180
|
+
sv10_days_avg: Decimal | None = Field(default=None, alias="sv10DaysAvg")
|
|
181
|
+
sv20_days_avg: Decimal | None = Field(default=None, alias="sv20DaysAvg")
|
|
182
|
+
sv1_mon_avg: Decimal | None = Field(default=None, alias="sv1MonAvg")
|
|
183
|
+
sv2_mon_avg: Decimal | None = Field(default=None, alias="sv2MonAvg")
|
|
184
|
+
sv3_mon_avg: Decimal | None = Field(default=None, alias="sv3MonAvg")
|
|
185
|
+
sv4_mon_avg: Decimal | None = Field(default=None, alias="sv4MonAvg")
|
|
186
|
+
sv6_mon_avg: Decimal | None = Field(default=None, alias="sv6MonAvg")
|
|
187
|
+
week52_high: Decimal | None = Field(default=None, alias="week52High")
|
|
188
|
+
week52_low: Decimal | None = Field(default=None, alias="week52Low")
|
|
189
|
+
week52_range: str | None = Field(default=None, alias="week52Range")
|
|
190
|
+
market_cap: Decimal | None = Field(default=None, alias="marketCap")
|
|
191
|
+
days_range: str | None = Field(default=None, alias="daysRange")
|
|
192
|
+
delta52_wk_high: Decimal | None = Field(default=None, alias="delta52WkHigh")
|
|
193
|
+
delta52_wk_low: Decimal | None = Field(default=None, alias="delta52WkLow")
|
|
194
|
+
currency: str | None = None
|
|
195
|
+
exchange: str | None = None
|
|
196
|
+
marginable: bool | None = None
|
|
197
|
+
bid_ask_spread: Decimal | None = Field(default=None, alias="bidAskSpread")
|
|
198
|
+
bid_size: int | None = Field(default=None, alias="bidSize")
|
|
199
|
+
ask_size: int | None = Field(default=None, alias="askSize")
|
|
200
|
+
open: Decimal | None = None
|
|
201
|
+
delta: Decimal | None = None
|
|
202
|
+
gamma: Decimal | None = None
|
|
203
|
+
iv_pct: Decimal | None = Field(default=None, alias="ivPct")
|
|
204
|
+
rho: Decimal | None = None
|
|
205
|
+
theta: Decimal | None = None
|
|
206
|
+
vega: Decimal | None = None
|
|
207
|
+
premium: Decimal | None = None
|
|
208
|
+
days_to_expiration: int | None = Field(default=None, alias="daysToExpiration")
|
|
209
|
+
intrinsic_value: Decimal | None = Field(default=None, alias="intrinsicValue")
|
|
210
|
+
open_interest: Decimal | None = Field(default=None, alias="openInterest")
|
|
211
|
+
options_adjusted_flag: bool | None = Field(default=None, alias="optionsAdjustedFlag")
|
|
212
|
+
deliverables_str: str | None = Field(default=None, alias="deliverablesStr")
|
|
213
|
+
option_multiplier: Decimal | None = Field(default=None, alias="optionMultiplier")
|
|
214
|
+
base_symbol_and_price: str | None = Field(default=None, alias="baseSymbolAndPrice")
|
|
215
|
+
est_earnings: Decimal | None = Field(default=None, alias="estEarnings")
|
|
216
|
+
eps: Decimal | None = None
|
|
217
|
+
pe_ratio: Decimal | None = Field(default=None, alias="peRatio")
|
|
218
|
+
annual_dividend: Decimal | None = Field(default=None, alias="annualDividend")
|
|
219
|
+
dividend: Decimal | None = None
|
|
220
|
+
div_yield: Decimal | None = Field(default=None, alias="divYield")
|
|
221
|
+
cusip: str | None = None
|
|
222
|
+
|
|
223
|
+
@field_validator("last_trade_time", mode="before")
|
|
224
|
+
@classmethod
|
|
225
|
+
def parse_last_trade_time(cls, value: object) -> datetime | None:
|
|
226
|
+
return parse_broker_datetime(value)
|
|
227
|
+
|
|
228
|
+
@field_validator("div_pay_date", "ex_dividend_date", mode="before")
|
|
229
|
+
@classmethod
|
|
230
|
+
def parse_dividend_dates(cls, value: object) -> date | None:
|
|
231
|
+
return parse_broker_date(value)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class PositionLot(BrokerModel):
|
|
235
|
+
position_id: int | None = Field(default=None, alias="positionId")
|
|
236
|
+
position_lot_id: int | None = Field(default=None, alias="positionLotId")
|
|
237
|
+
price: Decimal | None = None
|
|
238
|
+
term_code: int | None = Field(default=None, alias="termCode")
|
|
239
|
+
days_gain: Decimal | None = Field(default=None, alias="daysGain")
|
|
240
|
+
days_gain_pct: Decimal | None = Field(default=None, alias="daysGainPct")
|
|
241
|
+
market_value: Decimal | None = Field(default=None, alias="marketValue")
|
|
242
|
+
total_cost: Decimal | None = Field(default=None, alias="totalCost")
|
|
243
|
+
total_cost_for_gain_pct: Decimal | None = Field(default=None, alias="totalCostForGainPct")
|
|
244
|
+
total_gain: Decimal | None = Field(default=None, alias="totalGain")
|
|
245
|
+
lot_source_code: int | None = Field(default=None, alias="lotSourceCode")
|
|
246
|
+
original_qty: Decimal | None = Field(default=None, alias="originalQty")
|
|
247
|
+
remaining_qty: Decimal | None = Field(default=None, alias="remainingQty")
|
|
248
|
+
available_qty: Decimal | None = Field(default=None, alias="availableQty")
|
|
249
|
+
order_no: int | None = Field(default=None, alias="orderNo")
|
|
250
|
+
leg_no: int | None = Field(default=None, alias="legNo")
|
|
251
|
+
acquired_date: date | None = Field(default=None, alias="acquiredDate")
|
|
252
|
+
location_code: int | None = Field(default=None, alias="locationCode")
|
|
253
|
+
exchange_rate: Decimal | None = Field(default=None, alias="exchangeRate")
|
|
254
|
+
settlement_currency: str | None = Field(default=None, alias="settlementCurrency")
|
|
255
|
+
payment_currency: str | None = Field(default=None, alias="paymentCurrency")
|
|
256
|
+
adj_price: Decimal | None = Field(default=None, alias="adjPrice")
|
|
257
|
+
comm_per_share: Decimal | None = Field(default=None, alias="commPerShare")
|
|
258
|
+
fees_per_share: Decimal | None = Field(default=None, alias="feesPerShare")
|
|
259
|
+
premium_adj: Decimal | None = Field(default=None, alias="premiumAdj")
|
|
260
|
+
short_type: int | None = Field(default=None, alias="shortType")
|
|
261
|
+
|
|
262
|
+
@field_validator("acquired_date", mode="before")
|
|
263
|
+
@classmethod
|
|
264
|
+
def parse_acquired_date(cls, value: object) -> date | None:
|
|
265
|
+
return parse_broker_date(value)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _empty_position_lots() -> list[PositionLot]:
|
|
269
|
+
return []
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
class Position(BrokerModel):
|
|
273
|
+
position_id: int | None = Field(default=None, alias="positionId")
|
|
274
|
+
account_id: str | None = Field(default=None, alias="accountId")
|
|
275
|
+
product: PortfolioProduct | None = Field(default=None, alias="product")
|
|
276
|
+
osi_key: str | None = Field(default=None, alias="osiKey")
|
|
277
|
+
symbol_description: str | None = Field(default=None, alias="symbolDescription")
|
|
278
|
+
date_acquired: date | None = Field(default=None, alias="dateAcquired")
|
|
279
|
+
price_paid: Decimal | None = Field(default=None, alias="pricePaid")
|
|
280
|
+
price: Decimal | None = None
|
|
281
|
+
commissions: Decimal | None = None
|
|
282
|
+
other_fees: Decimal | None = Field(default=None, alias="otherFees")
|
|
283
|
+
quantity: Decimal | None = None
|
|
284
|
+
position_indicator: str | None = Field(default=None, alias="positionIndicator")
|
|
285
|
+
position_type: str | None = Field(default=None, alias="positionType")
|
|
286
|
+
change: Decimal | None = None
|
|
287
|
+
change_pct: Decimal | None = Field(default=None, alias="changePct")
|
|
288
|
+
days_gain: Decimal | None = Field(default=None, alias="daysGain")
|
|
289
|
+
days_gain_pct: Decimal | None = Field(default=None, alias="daysGainPct")
|
|
290
|
+
market_value: Decimal | None = Field(default=None, alias="marketValue")
|
|
291
|
+
total_cost: Decimal | None = Field(default=None, alias="totalCost")
|
|
292
|
+
total_gain: Decimal | None = Field(default=None, alias="totalGain")
|
|
293
|
+
total_gain_pct: Decimal | None = Field(default=None, alias="totalGainPct")
|
|
294
|
+
pct_of_portfolio: Decimal | None = Field(default=None, alias="pctOfPortfolio")
|
|
295
|
+
cost_per_share: Decimal | None = Field(default=None, alias="costPerShare")
|
|
296
|
+
quote_status: str | None = Field(default=None, alias="quoteStatus")
|
|
297
|
+
date_time_utc: datetime | None = Field(default=None, alias="dateTimeUTC")
|
|
298
|
+
adj_prev_close: Decimal | None = Field(default=None, alias="adjPrevClose")
|
|
299
|
+
performance: PerformanceView | None = None
|
|
300
|
+
fundamental: FundamentalView | None = None
|
|
301
|
+
options_watch: OptionsWatchView | None = Field(default=None, alias="optionsWatch")
|
|
302
|
+
quick: QuickView | None = None
|
|
303
|
+
complete: CompleteView | None = None
|
|
304
|
+
lots_details: str | None = Field(default=None, alias="lotsDetails")
|
|
305
|
+
quote_details: str | None = Field(default=None, alias="quoteDetails")
|
|
306
|
+
position_lots: list[PositionLot] = Field(
|
|
307
|
+
default_factory=_empty_position_lots, alias="positionLot"
|
|
308
|
+
)
|
|
309
|
+
today_commissions: Decimal | None = Field(default=None, alias="todayCommissions")
|
|
310
|
+
today_fees: Decimal | None = Field(default=None, alias="todayFees")
|
|
311
|
+
today_price_paid: Decimal | None = Field(default=None, alias="todayPricePaid")
|
|
312
|
+
today_quantity: Decimal | None = Field(default=None, alias="todayQuantity")
|
|
313
|
+
|
|
314
|
+
@field_validator("date_acquired", mode="before")
|
|
315
|
+
@classmethod
|
|
316
|
+
def parse_date_acquired(cls, value: object) -> date | None:
|
|
317
|
+
return parse_broker_date(value)
|
|
318
|
+
|
|
319
|
+
@field_validator("date_time_utc", mode="before")
|
|
320
|
+
@classmethod
|
|
321
|
+
def parse_date_time_utc(cls, value: object) -> datetime | None:
|
|
322
|
+
return parse_broker_datetime(value)
|
|
323
|
+
|
|
324
|
+
@model_validator(mode="before")
|
|
325
|
+
@classmethod
|
|
326
|
+
def normalize_nested_aliases(cls, value: object) -> object:
|
|
327
|
+
if not isinstance(value, dict):
|
|
328
|
+
return value
|
|
329
|
+
data = dict(cast(dict[str, Any], value))
|
|
330
|
+
for source, target in {
|
|
331
|
+
"Product": "product",
|
|
332
|
+
"Performance": "performance",
|
|
333
|
+
"Fundamental": "fundamental",
|
|
334
|
+
"OptionsWatch": "optionsWatch",
|
|
335
|
+
"Quick": "quick",
|
|
336
|
+
"Complete": "complete",
|
|
337
|
+
"PositionLot": "positionLot",
|
|
338
|
+
"quotestatus": "quoteStatus",
|
|
339
|
+
}.items():
|
|
340
|
+
if source in data and target not in data:
|
|
341
|
+
data[target] = data[source]
|
|
342
|
+
data.pop(source, None)
|
|
343
|
+
return data
|
|
344
|
+
|
|
345
|
+
@field_validator("position_lots", mode="before")
|
|
346
|
+
@classmethod
|
|
347
|
+
def normalize_position_lots(cls, value: object) -> object:
|
|
348
|
+
if value is None:
|
|
349
|
+
return []
|
|
350
|
+
if isinstance(value, dict):
|
|
351
|
+
return [cast(dict[str, Any], value)]
|
|
352
|
+
return value
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _empty_positions() -> list[Position]:
|
|
356
|
+
return []
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
class AccountPortfolio(BrokerModel):
|
|
360
|
+
account_id: str | None = Field(default=None, alias="accountId")
|
|
361
|
+
next: str | None = None
|
|
362
|
+
total_no_of_pages: int | None = Field(default=None, alias="totalNoOfPages")
|
|
363
|
+
total_pages: int | None = Field(default=None, alias="totalPages")
|
|
364
|
+
next_page_no: str | None = Field(default=None, alias="nextPageNo")
|
|
365
|
+
positions: list[Position] = Field(default_factory=_empty_positions, alias="position")
|
|
366
|
+
|
|
367
|
+
@model_validator(mode="before")
|
|
368
|
+
@classmethod
|
|
369
|
+
def normalize_response(cls, value: object) -> object:
|
|
370
|
+
if not isinstance(value, dict):
|
|
371
|
+
return value
|
|
372
|
+
data = dict(cast(dict[str, Any], value))
|
|
373
|
+
if "Position" in data and "position" not in data:
|
|
374
|
+
data["position"] = data["Position"]
|
|
375
|
+
data.pop("Position", None)
|
|
376
|
+
return data
|
|
377
|
+
|
|
378
|
+
@field_validator("positions", mode="before")
|
|
379
|
+
@classmethod
|
|
380
|
+
def normalize_positions(cls, value: object) -> object:
|
|
381
|
+
if value is None:
|
|
382
|
+
return []
|
|
383
|
+
if isinstance(value, dict):
|
|
384
|
+
return [cast(dict[str, Any], value)]
|
|
385
|
+
return value
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
class PortfolioTotals(BrokerModel):
|
|
389
|
+
todays_gain_loss: Decimal | None = Field(default=None, alias="todaysGainLoss")
|
|
390
|
+
todays_gain_loss_pct: Decimal | None = Field(default=None, alias="todaysGainLossPct")
|
|
391
|
+
total_market_value: Decimal | None = Field(default=None, alias="totalMarketValue")
|
|
392
|
+
total_gain_loss: Decimal | None = Field(default=None, alias="totalGainLoss")
|
|
393
|
+
total_gain_loss_pct: Decimal | None = Field(default=None, alias="totalGainLossPct")
|
|
394
|
+
total_price_paid: Decimal | None = Field(default=None, alias="totalPricePaid")
|
|
395
|
+
cash_balance: Decimal | None = Field(default=None, alias="cashBalance")
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _empty_account_portfolios() -> list[AccountPortfolio]:
|
|
399
|
+
return []
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
class PortfolioResponse(BrokerModel):
|
|
403
|
+
totals: PortfolioTotals | None = None
|
|
404
|
+
account_portfolios: list[AccountPortfolio] = Field(
|
|
405
|
+
default_factory=_empty_account_portfolios, alias="accountPortfolio"
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
@model_validator(mode="before")
|
|
409
|
+
@classmethod
|
|
410
|
+
def normalize_response(cls, value: object) -> object:
|
|
411
|
+
if not isinstance(value, dict):
|
|
412
|
+
return value
|
|
413
|
+
data = dict(cast(dict[str, Any], value))
|
|
414
|
+
if "Totals" in data and "totals" not in data:
|
|
415
|
+
data["totals"] = data["Totals"]
|
|
416
|
+
if "AccountPortfolio" in data and "accountPortfolio" not in data:
|
|
417
|
+
data["accountPortfolio"] = data["AccountPortfolio"]
|
|
418
|
+
data.pop("Totals", None)
|
|
419
|
+
data.pop("AccountPortfolio", None)
|
|
420
|
+
return data
|
|
421
|
+
|
|
422
|
+
@field_validator("account_portfolios", mode="before")
|
|
423
|
+
@classmethod
|
|
424
|
+
def normalize_account_portfolios(cls, value: object) -> object:
|
|
425
|
+
if value is None:
|
|
426
|
+
return []
|
|
427
|
+
if isinstance(value, dict):
|
|
428
|
+
return [cast(dict[str, Any], value)]
|
|
429
|
+
return value
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Portfolio API service."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncIterator
|
|
4
|
+
from typing import Any, cast
|
|
5
|
+
|
|
6
|
+
from pydantic import ValidationError
|
|
7
|
+
|
|
8
|
+
from etrade_python.exceptions import ETradeResponseError, ETradeValidationError
|
|
9
|
+
from etrade_python.portfolio.models import PortfolioRequest, PortfolioResponse, Position
|
|
10
|
+
from etrade_python.transport.http import ApiTransport
|
|
11
|
+
from etrade_python.transport.response import (
|
|
12
|
+
JsonValue,
|
|
13
|
+
raise_response_validation_error,
|
|
14
|
+
validate_response_shape,
|
|
15
|
+
)
|
|
16
|
+
from etrade_python.transport.retry import RetrySafety
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PortfolioService:
|
|
20
|
+
"""Access E*TRADE portfolio endpoints."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, transport: ApiTransport) -> None:
|
|
23
|
+
self._transport = transport
|
|
24
|
+
|
|
25
|
+
async def get_positions(
|
|
26
|
+
self, account_id_key: str, request: PortfolioRequest | None = None
|
|
27
|
+
) -> PortfolioResponse:
|
|
28
|
+
if not account_id_key.strip():
|
|
29
|
+
raise ETradeValidationError("account_id_key is required")
|
|
30
|
+
|
|
31
|
+
portfolio_request = request or PortfolioRequest()
|
|
32
|
+
response = await self._transport.request(
|
|
33
|
+
"GET",
|
|
34
|
+
f"/v1/accounts/{account_id_key}/portfolio.json",
|
|
35
|
+
params=portfolio_request.query_params(),
|
|
36
|
+
safety=RetrySafety.SAFE_READ,
|
|
37
|
+
operation="portfolio.positions",
|
|
38
|
+
)
|
|
39
|
+
if response.status_code == 204 and response.data is None:
|
|
40
|
+
return PortfolioResponse(accountPortfolio=[])
|
|
41
|
+
data = _require_mapping(response.data)
|
|
42
|
+
portfolio_data = _unwrap(data, "PortfolioResponse")
|
|
43
|
+
try:
|
|
44
|
+
return validate_response_shape(
|
|
45
|
+
PortfolioResponse.model_validate(portfolio_data),
|
|
46
|
+
"portfolio",
|
|
47
|
+
collection_field="account_portfolios",
|
|
48
|
+
)
|
|
49
|
+
except ValidationError as error:
|
|
50
|
+
raise_response_validation_error("Invalid portfolio response", error)
|
|
51
|
+
|
|
52
|
+
async def iter_positions(
|
|
53
|
+
self, account_id_key: str, request: PortfolioRequest | None = None
|
|
54
|
+
) -> AsyncIterator[Position]:
|
|
55
|
+
"""Iterate positions using the broker's nextPageNo continuation field."""
|
|
56
|
+
base_request = request or PortfolioRequest()
|
|
57
|
+
current_request = base_request
|
|
58
|
+
seen_pages = {base_request.page_number or 1}
|
|
59
|
+
while True:
|
|
60
|
+
page = await self.get_positions(account_id_key, current_request)
|
|
61
|
+
next_page = _next_page_number(page)
|
|
62
|
+
if next_page in seen_pages:
|
|
63
|
+
raise ETradeResponseError("Portfolio pagination did not advance")
|
|
64
|
+
for account_portfolio in page.account_portfolios:
|
|
65
|
+
for position in account_portfolio.positions:
|
|
66
|
+
yield position
|
|
67
|
+
if next_page is None:
|
|
68
|
+
return
|
|
69
|
+
seen_pages.add(next_page)
|
|
70
|
+
current_request = base_request.model_copy(update={"page_number": next_page})
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _require_mapping(data: JsonValue) -> dict[str, Any]:
|
|
74
|
+
if not isinstance(data, dict):
|
|
75
|
+
raise ETradeResponseError("Invalid portfolio response")
|
|
76
|
+
return cast(dict[str, Any], data)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _unwrap(data: dict[str, Any], envelope: str) -> dict[str, Any]:
|
|
80
|
+
value = data.get(envelope, data)
|
|
81
|
+
if not isinstance(value, dict):
|
|
82
|
+
raise ETradeResponseError("Invalid portfolio response")
|
|
83
|
+
return cast(dict[str, Any], value)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _next_page_number(response: PortfolioResponse) -> int | None:
|
|
87
|
+
for account_portfolio in response.account_portfolios:
|
|
88
|
+
page_number = account_portfolio.next_page_no
|
|
89
|
+
if page_number:
|
|
90
|
+
try:
|
|
91
|
+
next_page = int(page_number)
|
|
92
|
+
except ValueError:
|
|
93
|
+
raise ETradeResponseError("Invalid portfolio pagination page number") from None
|
|
94
|
+
if next_page < 1:
|
|
95
|
+
raise ETradeResponseError("Invalid portfolio pagination page number")
|
|
96
|
+
return next_page
|
|
97
|
+
if account_portfolio.next:
|
|
98
|
+
raise ETradeResponseError("Portfolio pagination is missing the next page number")
|
|
99
|
+
return None
|
etrade_python/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Transactions API service and models."""
|
|
2
|
+
|
|
3
|
+
from etrade_python.transactions.models import (
|
|
4
|
+
ProductId,
|
|
5
|
+
Transaction,
|
|
6
|
+
TransactionBrokerage,
|
|
7
|
+
TransactionCategory,
|
|
8
|
+
TransactionDetailsRequest,
|
|
9
|
+
TransactionDetailsResponse,
|
|
10
|
+
TransactionProduct,
|
|
11
|
+
TransactionsRequest,
|
|
12
|
+
TransactionsResponse,
|
|
13
|
+
)
|
|
14
|
+
from etrade_python.transactions.service import TransactionsService
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"ProductId",
|
|
18
|
+
"Transaction",
|
|
19
|
+
"TransactionBrokerage",
|
|
20
|
+
"TransactionCategory",
|
|
21
|
+
"TransactionDetailsRequest",
|
|
22
|
+
"TransactionDetailsResponse",
|
|
23
|
+
"TransactionProduct",
|
|
24
|
+
"TransactionsRequest",
|
|
25
|
+
"TransactionsResponse",
|
|
26
|
+
"TransactionsService",
|
|
27
|
+
]
|