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,811 @@
1
+ """Typed market request and response models."""
2
+
3
+ from datetime import 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_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 QuotesRequest(BaseModel):
25
+ """Query parameters for quote lookup."""
26
+
27
+ model_config = ConfigDict(frozen=True)
28
+
29
+ detail_flag: str | None = None
30
+ require_earnings_date: bool | None = None
31
+ override_symbol_count: bool | None = None
32
+ skip_mini_options_check: bool | None = None
33
+
34
+ @field_validator("detail_flag")
35
+ @classmethod
36
+ def valid_detail_flag(cls, value: str | None) -> str | None:
37
+ if value is None:
38
+ return None
39
+ normalized = value.strip().upper()
40
+ if not normalized:
41
+ raise ValueError("A nonempty value is required")
42
+ if normalized not in {"ALL", "FUNDAMENTAL", "INTRADAY", "OPTIONS", "WEEK_52", "MF_DETAIL"}:
43
+ raise ValueError("Unsupported quote detail flag")
44
+ return normalized
45
+
46
+ def query_params(self) -> dict[str, str | None]:
47
+ return {
48
+ "detailFlag": self.detail_flag,
49
+ "requireEarningsDate": _bool_query(self.require_earnings_date),
50
+ "overrideSymbolCount": _bool_query(self.override_symbol_count),
51
+ "skipMiniOptionsCheck": _bool_query(self.skip_mini_options_check),
52
+ }
53
+
54
+
55
+ class ProductLookupRequest(BaseModel):
56
+ """Path parameter for product lookup."""
57
+
58
+ model_config = ConfigDict(frozen=True)
59
+
60
+ search: str
61
+
62
+ @field_validator("search")
63
+ @classmethod
64
+ def nonempty_search(cls, value: str) -> str:
65
+ if not value.strip():
66
+ raise ValueError("A nonempty value is required")
67
+ return value
68
+
69
+
70
+ class OptionExpirationsRequest(BaseModel):
71
+ """Query parameters for option expiration dates."""
72
+
73
+ model_config = ConfigDict(frozen=True)
74
+
75
+ expiry_type: str | None = None
76
+
77
+ @field_validator("expiry_type")
78
+ @classmethod
79
+ def valid_expiry_type(cls, value: str | None) -> str | None:
80
+ if value is None:
81
+ return None
82
+ normalized = value.strip().upper()
83
+ if not normalized:
84
+ raise ValueError("A nonempty value is required")
85
+ if normalized not in {
86
+ "UNSPECIFIED",
87
+ "DAILY",
88
+ "WEEKLY",
89
+ "MONTHLY",
90
+ "QUARTERLY",
91
+ "VIX",
92
+ "ALL",
93
+ "MONTHEND",
94
+ }:
95
+ raise ValueError("Unsupported option expiration type")
96
+ return normalized
97
+
98
+ def query_params(self, symbol: str) -> dict[str, str | None]:
99
+ return {"symbol": symbol, "expiryType": self.expiry_type}
100
+
101
+
102
+ class OptionChainRequest(BaseModel):
103
+ """Query parameters for option chains."""
104
+
105
+ model_config = ConfigDict(frozen=True)
106
+
107
+ expiry_year: int | None = Field(default=None, ge=1900)
108
+ expiry_month: int | None = Field(default=None, ge=1, le=12)
109
+ expiry_day: int | None = Field(default=None, ge=1, le=31)
110
+ strike_price_near: Decimal | None = Field(default=None, ge=0)
111
+ no_of_strikes: int | None = Field(default=None, ge=1)
112
+ include_weekly: bool | None = None
113
+ skip_adjusted: bool | None = None
114
+ option_category: str | None = None
115
+ chain_type: str | None = None
116
+ price_type: str | None = None
117
+
118
+ @field_validator("option_category")
119
+ @classmethod
120
+ def valid_option_category(cls, value: str | None) -> str | None:
121
+ return _normalize_choice(value, {"STANDARD", "ALL", "MINI"}, "option category")
122
+
123
+ @field_validator("chain_type")
124
+ @classmethod
125
+ def valid_chain_type(cls, value: str | None) -> str | None:
126
+ return _normalize_choice(value, {"CALL", "PUT", "CALLPUT"}, "chain type")
127
+
128
+ @field_validator("price_type")
129
+ @classmethod
130
+ def valid_price_type(cls, value: str | None) -> str | None:
131
+ return _normalize_choice(value, {"ATNM", "ALL"}, "price type")
132
+
133
+ def query_params(self, symbol: str) -> dict[str, str | int | None]:
134
+ return {
135
+ "symbol": symbol,
136
+ "expiryYear": self.expiry_year,
137
+ "expiryMonth": self.expiry_month,
138
+ "expiryDay": self.expiry_day,
139
+ "strikePriceNear": str(self.strike_price_near)
140
+ if self.strike_price_near is not None
141
+ else None,
142
+ "noOfStrikes": self.no_of_strikes,
143
+ "includeWeekly": _bool_query(self.include_weekly),
144
+ "skipAdjusted": _bool_query(self.skip_adjusted),
145
+ "optionCategory": self.option_category,
146
+ "chainType": self.chain_type,
147
+ "priceType": self.price_type,
148
+ }
149
+
150
+
151
+ def _normalize_choice(value: str | None, choices: set[str], label: str) -> str | None:
152
+ if value is None:
153
+ return None
154
+ normalized = value.strip().upper()
155
+ if not normalized:
156
+ raise ValueError("A nonempty value is required")
157
+ if normalized not in choices:
158
+ raise ValueError(f"Unsupported {label}")
159
+ return normalized
160
+
161
+
162
+ class ProductId(BrokerModel):
163
+ symbol: str | None = None
164
+ type_code: str | None = Field(default=None, alias="typeCode")
165
+
166
+
167
+ class Product(BrokerModel):
168
+ symbol: str | None = None
169
+ security_type: str | None = Field(default=None, alias="securityType")
170
+ security_sub_type: str | None = Field(default=None, alias="securitySubType")
171
+ call_put: str | None = Field(default=None, alias="callPut")
172
+ expiry_year: int | None = Field(default=None, alias="expiryYear")
173
+ expiry_month: int | None = Field(default=None, alias="expiryMonth")
174
+ expiry_day: int | None = Field(default=None, alias="expiryDay")
175
+ strike_price: Decimal | None = Field(default=None, alias="strikePrice")
176
+ expiry_type: str | None = Field(default=None, alias="expiryType")
177
+ product_id: ProductId | None = Field(default=None, alias="productId")
178
+
179
+ @model_validator(mode="before")
180
+ @classmethod
181
+ def normalize_nested_aliases(cls, value: object) -> object:
182
+ if not isinstance(value, dict):
183
+ return value
184
+ data = dict(cast(dict[str, Any], value))
185
+ if "ProductId" in data and "productId" not in data:
186
+ data["productId"] = data["ProductId"]
187
+ data.pop("ProductId", None)
188
+ return data
189
+
190
+
191
+ class LookupProduct(BrokerModel):
192
+ symbol: str | None = None
193
+ description: str | None = None
194
+ type: str | None = None
195
+
196
+
197
+ def _empty_lookup_products() -> list[LookupProduct]:
198
+ return []
199
+
200
+
201
+ class ProductLookupResponse(BrokerModel):
202
+ products: list[LookupProduct] = Field(default_factory=_empty_lookup_products, alias="data")
203
+
204
+ @model_validator(mode="before")
205
+ @classmethod
206
+ def normalize_response(cls, value: object) -> object:
207
+ if not isinstance(value, dict):
208
+ return value
209
+ data = dict(cast(dict[str, Any], value))
210
+ if "Data" in data and "data" not in data:
211
+ data["data"] = data["Data"]
212
+ data.pop("Data", None)
213
+ return data
214
+
215
+ @field_validator("products", mode="before")
216
+ @classmethod
217
+ def normalize_products(cls, value: object) -> object:
218
+ if value is None:
219
+ return []
220
+ if isinstance(value, dict):
221
+ return [cast(dict[str, Any], value)]
222
+ return value
223
+
224
+
225
+ def _normalize_aliases(value: object, aliases: dict[str, str]) -> object:
226
+ if not isinstance(value, dict):
227
+ return value
228
+ data = dict(cast(dict[str, Any], value))
229
+ for source, target in aliases.items():
230
+ if source in data and target not in data:
231
+ data[target] = data[source]
232
+ data.pop(source, None)
233
+ return data
234
+
235
+
236
+ def _normalize_collection(value: object, wrappers: tuple[str, ...] = ()) -> object:
237
+ if isinstance(value, dict):
238
+ data = cast(dict[str, Any], value)
239
+ for key in wrappers:
240
+ if key in data:
241
+ value = data[key]
242
+ break
243
+ if value is None or value == {}:
244
+ return []
245
+ if isinstance(value, dict):
246
+ return [cast(dict[str, Any], value)]
247
+ return value
248
+
249
+
250
+ class OptionGreeks(BrokerModel):
251
+ rho: Decimal | None = None
252
+ vega: Decimal | None = None
253
+ theta: Decimal | None = None
254
+ delta: Decimal | None = None
255
+ gamma: Decimal | None = None
256
+ iv: Decimal | None = None
257
+ current_value: bool | str | None = Field(default=None, alias="currentValue")
258
+
259
+
260
+ class ExtendedHourQuoteDetails(BrokerModel):
261
+ last_price: Decimal | None = Field(default=None, alias="lastPrice")
262
+ change: Decimal | None = None
263
+ percent_change: Decimal | None = Field(default=None, alias="percentChange")
264
+ bid: Decimal | None = None
265
+ bid_size: int | None = Field(default=None, alias="bidSize")
266
+ ask: Decimal | None = None
267
+ ask_size: int | None = Field(default=None, alias="askSize")
268
+ volume: int | None = None
269
+ time_of_last_trade: datetime | None = Field(default=None, alias="timeOfLastTrade")
270
+ time_zone: str | None = Field(default=None, alias="timeZone")
271
+ quote_status: str | None = Field(default=None, alias="quoteStatus")
272
+
273
+ @field_validator("time_of_last_trade", mode="before")
274
+ @classmethod
275
+ def parse_time_of_last_trade(cls, value: object) -> datetime | None:
276
+ return parse_broker_datetime(value)
277
+
278
+
279
+ class OptionDeliverable(BrokerModel):
280
+ root_symbol: str | None = Field(default=None, alias="rootSymbol")
281
+ deliverable_symbol: str | None = Field(default=None, alias="deliverableSymbol")
282
+ deliverable_type_code: str | None = Field(default=None, alias="deliverableTypeCode")
283
+ deliverable_exchange_code: str | None = Field(default=None, alias="deliverableExchangeCode")
284
+ deliverable_strike_percent: Decimal | None = Field(
285
+ default=None, alias="deliverableStrikePercent"
286
+ )
287
+ deliverable_cil_shares: Decimal | None = Field(default=None, alias="deliverableCILShares")
288
+ deliverable_whole_shares: int | None = Field(default=None, alias="deliverableWholeShares")
289
+
290
+
291
+ class QuoteMessage(BrokerModel):
292
+ description: str | None = None
293
+ code: int | None = None
294
+ type: str | None = None
295
+
296
+
297
+ def _empty_quote_messages() -> list[QuoteMessage]:
298
+ return []
299
+
300
+
301
+ class QuoteMessages(BrokerModel):
302
+ messages: list[QuoteMessage] = Field(default_factory=_empty_quote_messages, alias="message")
303
+
304
+ @model_validator(mode="before")
305
+ @classmethod
306
+ def normalize_response(cls, value: object) -> object:
307
+ return _normalize_aliases(value, {"Message": "message", "messages": "message"})
308
+
309
+ @field_validator("messages", mode="before")
310
+ @classmethod
311
+ def normalize_messages(cls, value: object) -> object:
312
+ return _normalize_collection(value)
313
+
314
+
315
+ class NetAsset(BrokerModel):
316
+ value: Decimal | None = None
317
+ as_of_date: datetime | None = Field(default=None, alias="asOfDate")
318
+
319
+ @field_validator("as_of_date", mode="before")
320
+ @classmethod
321
+ def parse_as_of_date(cls, value: object) -> datetime | None:
322
+ return parse_broker_datetime(value)
323
+
324
+
325
+ class RedemptionValues(BrokerModel):
326
+ low: str | None = None
327
+ high: str | None = None
328
+ percent: str | None = None
329
+
330
+
331
+ class SaleChargeValues(BrokerModel):
332
+ lowhigh: str | None = None
333
+ percent: str | None = None
334
+
335
+
336
+ def _empty_front_end_values() -> list[RedemptionValues]:
337
+ return []
338
+
339
+
340
+ def _empty_sales_values() -> list[RedemptionValues]:
341
+ return []
342
+
343
+
344
+ class Redemption(BrokerModel):
345
+ min_month: str | None = Field(default=None, alias="minMonth")
346
+ fee_percent: str | None = Field(default=None, alias="feePercent")
347
+ is_front_end: str | None = Field(default=None, alias="isFrontEnd")
348
+ front_end_values: list[RedemptionValues] = Field(
349
+ default_factory=_empty_front_end_values, alias="frontEndValues"
350
+ )
351
+ redemption_duration_type: str | None = Field(default=None, alias="redemptionDurationType")
352
+ is_sales: str | None = Field(default=None, alias="isSales")
353
+ sales_duration_type: str | None = Field(default=None, alias="salesDurationType")
354
+ sales_values: list[RedemptionValues] = Field(
355
+ default_factory=_empty_sales_values, alias="salesValues"
356
+ )
357
+
358
+ @model_validator(mode="before")
359
+ @classmethod
360
+ def normalize_response(cls, value: object) -> object:
361
+ return _normalize_aliases(
362
+ value, {"FrontEndValues": "frontEndValues", "SalesValues": "salesValues"}
363
+ )
364
+
365
+ @field_validator("front_end_values", "sales_values", mode="before")
366
+ @classmethod
367
+ def normalize_values(cls, value: object) -> object:
368
+ return _normalize_collection(value, ("Values", "values"))
369
+
370
+
371
+ def _empty_option_deliverables() -> list[OptionDeliverable]:
372
+ return []
373
+
374
+
375
+ class QuoteDetails(BrokerModel):
376
+ adjusted_flag: bool | str | None = Field(default=None, alias="adjustedFlag")
377
+ ask: Decimal | None = None
378
+ ask_size: int | None = Field(default=None, alias="askSize")
379
+ ask_time: datetime | None = Field(default=None, alias="askTime")
380
+ bid: Decimal | None = None
381
+ bid_exchange: str | None = Field(default=None, alias="bidExchange")
382
+ bid_size: int | None = Field(default=None, alias="bidSize")
383
+ bid_time: datetime | None = Field(default=None, alias="bidTime")
384
+ change_close: Decimal | None = Field(default=None, alias="changeClose")
385
+ change_close_percentage: Decimal | None = Field(default=None, alias="changeClosePercentage")
386
+ company_name: str | None = Field(default=None, alias="companyName")
387
+ days_to_expiration: int | None = Field(default=None, alias="daysToExpiration")
388
+ dividend: Decimal | None = None
389
+ eps: Decimal | None = None
390
+ est_earnings: Decimal | None = Field(default=None, alias="estEarnings")
391
+ ex_dividend_date: datetime | None = Field(default=None, alias="exDividendDate")
392
+ high: Decimal | None = None
393
+ high52: Decimal | None = None
394
+ last_trade: Decimal | None = Field(default=None, alias="lastTrade")
395
+ low: Decimal | None = None
396
+ low52: Decimal | None = None
397
+ open: Decimal | None = None
398
+ open_interest: int | None = Field(default=None, alias="openInterest")
399
+ option_style: str | None = Field(default=None, alias="optionStyle")
400
+ option_underlier: str | None = Field(default=None, alias="optionUnderlier")
401
+ previous_close: Decimal | None = Field(default=None, alias="previousClose")
402
+ previous_day_volume: int | None = Field(default=None, alias="previousDayVolume")
403
+ primary_exchange: str | None = Field(default=None, alias="primaryExchange")
404
+ symbol_description: str | None = Field(default=None, alias="symbolDescription")
405
+ total_volume: int | None = Field(default=None, alias="totalVolume")
406
+ market_cap: Decimal | None = Field(default=None, alias="marketCap")
407
+ shares_outstanding: Decimal | None = Field(default=None, alias="sharesOutstanding")
408
+ next_earning_date: datetime | None = Field(default=None, alias="nextEarningDate")
409
+ beta: Decimal | None = None
410
+ yield_: Decimal | None = Field(default=None, alias="yield")
411
+ declared_dividend: Decimal | None = Field(default=None, alias="declaredDividend")
412
+ dividend_payable_date: datetime | None = Field(default=None, alias="dividendPayableDate")
413
+ pe: Decimal | None = None
414
+ week52_low_date: datetime | None = Field(default=None, alias="week52LowDate")
415
+ week52_hi_date: datetime | None = Field(default=None, alias="week52HiDate")
416
+ intrinsic_value: Decimal | None = Field(default=None, alias="intrinsicValue")
417
+ time_premium: Decimal | None = Field(default=None, alias="timePremium")
418
+ option_multiplier: Decimal | None = Field(default=None, alias="optionMultiplier")
419
+ contract_size: Decimal | None = Field(default=None, alias="contractSize")
420
+ expiration_date: datetime | None = Field(default=None, alias="expirationDate")
421
+ time_of_last_trade: datetime | None = Field(default=None, alias="timeOfLastTrade")
422
+ average_volume: int | None = Field(default=None, alias="averageVolume")
423
+ dir_last: str | None = Field(default=None, alias="dirLast")
424
+ option_underlier_exchange: str | None = Field(default=None, alias="optionUnderlierExchange")
425
+ upc: int | None = None
426
+ cash_deliverable: Decimal | None = Field(default=None, alias="cashDeliverable")
427
+ option_previous_bid_price: Decimal | None = Field(default=None, alias="optionPreviousBidPrice")
428
+ option_previous_ask_price: Decimal | None = Field(default=None, alias="optionPreviousAskPrice")
429
+ osi_key: str | None = Field(default=None, alias="osiKey")
430
+ eh_quote: ExtendedHourQuoteDetails | None = Field(default=None, alias="ehQuote")
431
+ option_greeks: OptionGreeks | None = Field(default=None, alias="optionGreeks")
432
+ option_deliverables: list[OptionDeliverable] = Field(
433
+ default_factory=_empty_option_deliverables, alias="optionDeliverableList"
434
+ )
435
+ perf12_months: Decimal | None = Field(default=None, alias="perf12Months")
436
+
437
+ @field_validator(
438
+ "ask_time",
439
+ "bid_time",
440
+ "ex_dividend_date",
441
+ "next_earning_date",
442
+ "dividend_payable_date",
443
+ "week52_low_date",
444
+ "week52_hi_date",
445
+ "expiration_date",
446
+ "time_of_last_trade",
447
+ mode="before",
448
+ )
449
+ @classmethod
450
+ def parse_detail_datetimes(cls, value: object) -> datetime | None:
451
+ return parse_broker_datetime(value)
452
+
453
+ @model_validator(mode="before")
454
+ @classmethod
455
+ def normalize_detail_aliases(cls, value: object) -> object:
456
+ return _normalize_aliases(
457
+ value,
458
+ {
459
+ "EhQuote": "ehQuote",
460
+ "OptionGreeks": "optionGreeks",
461
+ "OptionDeliverableList": "optionDeliverableList",
462
+ },
463
+ )
464
+
465
+ @field_validator("option_deliverables", mode="before")
466
+ @classmethod
467
+ def normalize_deliverables(cls, value: object) -> object:
468
+ return _normalize_collection(value, ("optionDeliverable", "OptionDeliverable"))
469
+
470
+
471
+ def _empty_deferred_sales_charges() -> list[SaleChargeValues]:
472
+ return []
473
+
474
+
475
+ def _empty_front_end_sales_charges() -> list[SaleChargeValues]:
476
+ return []
477
+
478
+
479
+ class MutualFundQuoteDetails(QuoteDetails):
480
+ cusip: str | None = None
481
+ transaction_fee: str | None = Field(default=None, alias="transactionFee")
482
+ early_redemption_fee: str | None = Field(default=None, alias="earlyRedemptionFee")
483
+ availability: str | None = None
484
+ initial_investment: Decimal | None = Field(default=None, alias="initialInvestment")
485
+ subsequent_investment: Decimal | None = Field(default=None, alias="subsequentInvestment")
486
+ fund_family: str | None = Field(default=None, alias="fundFamily")
487
+ fund_name: str | None = Field(default=None, alias="fundName")
488
+ net_asset_value: Decimal | None = Field(default=None, alias="netAssetValue")
489
+ public_offer_price: Decimal | None = Field(default=None, alias="publicOfferPrice")
490
+ net_expense_ratio: Decimal | None = Field(default=None, alias="netExpenseRatio")
491
+ gross_expense_ratio: Decimal | None = Field(default=None, alias="grossExpenseRatio")
492
+ order_cutoff_time: datetime | None = Field(default=None, alias="orderCutoffTime")
493
+ sales_charge: str | None = Field(default=None, alias="salesCharge")
494
+ initial_ira_investment: Decimal | None = Field(default=None, alias="initialIraInvestment")
495
+ subsequent_ira_investment: Decimal | None = Field(default=None, alias="subsequentIraInvestment")
496
+ net_assets: NetAsset | None = Field(default=None, alias="netAssets")
497
+ fund_inception_date: datetime | None = Field(default=None, alias="fundInceptionDate")
498
+ average_annual_returns: Decimal | None = Field(default=None, alias="averageAnnualReturns")
499
+ seven_day_current_yield: Decimal | None = Field(default=None, alias="sevenDayCurrentYield")
500
+ annual_total_return: Decimal | None = Field(default=None, alias="annualTotalReturn")
501
+ weighted_average_maturity: Decimal | None = Field(default=None, alias="weightedAverageMaturity")
502
+ average_annual_return1_yr: Decimal | None = Field(default=None, alias="averageAnnualReturn1Yr")
503
+ average_annual_return3_yr: Decimal | None = Field(default=None, alias="averageAnnualReturn3Yr")
504
+ average_annual_return5_yr: Decimal | None = Field(default=None, alias="averageAnnualReturn5Yr")
505
+ average_annual_return10_yr: Decimal | None = Field(
506
+ default=None, alias="averageAnnualReturn10Yr"
507
+ )
508
+ exchange_name: str | None = Field(default=None, alias="exchangeName")
509
+ since_inception: Decimal | None = Field(default=None, alias="sinceInception")
510
+ quarterly_since_inception: Decimal | None = Field(default=None, alias="quarterlySinceInception")
511
+ actual12_b1_fee: Decimal | None = Field(default=None, alias="actual12B1Fee")
512
+ performance_as_of_date: datetime | None = Field(default=None, alias="performanceAsOfDate")
513
+ qtrly_performance_as_of_date: datetime | None = Field(
514
+ default=None, alias="qtrlyPerformanceAsOfDate"
515
+ )
516
+ redemption: Redemption | None = None
517
+ morning_star_category: str | None = Field(default=None, alias="morningStarCategory")
518
+ monthly_trailing_return1_y: Decimal | None = Field(
519
+ default=None, alias="monthlyTrailingReturn1Y"
520
+ )
521
+ monthly_trailing_return3_y: Decimal | None = Field(
522
+ default=None, alias="monthlyTrailingReturn3Y"
523
+ )
524
+ monthly_trailing_return5_y: Decimal | None = Field(
525
+ default=None, alias="monthlyTrailingReturn5Y"
526
+ )
527
+ monthly_trailing_return10_y: Decimal | None = Field(
528
+ default=None, alias="monthlyTrailingReturn10Y"
529
+ )
530
+ etrade_early_redemption_fee: str | None = Field(default=None, alias="etradeEarlyRedemptionFee")
531
+ max_sales_load: Decimal | None = Field(default=None, alias="maxSalesLoad")
532
+ monthly_trailing_return_ytd: Decimal | None = Field(
533
+ default=None, alias="monthlyTrailingReturnYTD"
534
+ )
535
+ monthly_trailing_return1_m: Decimal | None = Field(
536
+ default=None, alias="monthlyTrailingReturn1M"
537
+ )
538
+ monthly_trailing_return3_m: Decimal | None = Field(
539
+ default=None, alias="monthlyTrailingReturn3M"
540
+ )
541
+ monthly_trailing_return6_m: Decimal | None = Field(
542
+ default=None, alias="monthlyTrailingReturn6M"
543
+ )
544
+ qtrly_trailing_return_ytd: Decimal | None = Field(default=None, alias="qtrlyTrailingReturnYTD")
545
+ qtrly_trailing_return1_m: Decimal | None = Field(default=None, alias="qtrlyTrailingReturn1M")
546
+ qtrly_trailing_return3_m: Decimal | None = Field(default=None, alias="qtrlyTrailingReturn3M")
547
+ qtrly_trailing_return6_m: Decimal | None = Field(default=None, alias="qtrlyTrailingReturn6M")
548
+ deferred_sales_charges: list[SaleChargeValues] = Field(
549
+ default_factory=_empty_deferred_sales_charges, alias="deferredSalesCharges"
550
+ )
551
+ front_end_sales_charges: list[SaleChargeValues] = Field(
552
+ default_factory=_empty_front_end_sales_charges, alias="frontEndSalesCharges"
553
+ )
554
+ exchange_code: str | None = Field(default=None, alias="exchangeCode")
555
+
556
+ @field_validator(
557
+ "order_cutoff_time",
558
+ "fund_inception_date",
559
+ "performance_as_of_date",
560
+ "qtrly_performance_as_of_date",
561
+ mode="before",
562
+ )
563
+ @classmethod
564
+ def parse_fund_datetimes(cls, value: object) -> datetime | None:
565
+ return parse_broker_datetime(value)
566
+
567
+ @model_validator(mode="before")
568
+ @classmethod
569
+ def normalize_fund_aliases(cls, value: object) -> object:
570
+ return _normalize_aliases(
571
+ value,
572
+ {
573
+ "NetAssets": "netAssets",
574
+ "Redemption": "redemption",
575
+ "DeferredSalesCharges": "deferredSalesCharges",
576
+ "FrontEndSalesCharges": "frontEndSalesCharges",
577
+ },
578
+ )
579
+
580
+ @field_validator("deferred_sales_charges", "front_end_sales_charges", mode="before")
581
+ @classmethod
582
+ def normalize_sales_charges(cls, value: object) -> object:
583
+ return _normalize_collection(value, ("saleChargeValues", "SaleChargeValues"))
584
+
585
+
586
+ class Quote(BrokerModel):
587
+ date_time: datetime | None = Field(default=None, alias="dateTime")
588
+ date_time_utc: datetime | None = Field(default=None, alias="dateTimeUTC")
589
+ quote_status: str | None = Field(default=None, alias="quoteStatus")
590
+ ah_flag: bool | str | None = Field(default=None, alias="ahFlag")
591
+ error_message: str | None = Field(default=None, alias="errorMessage")
592
+ time_zone: str | None = Field(default=None, alias="timeZone")
593
+ dst_flag: bool | str | None = Field(default=None, alias="dstFlag")
594
+ has_mini_options: bool | str | None = Field(default=None, alias="hasMiniOptions")
595
+ all: QuoteDetails | None = None
596
+ fundamental: QuoteDetails | None = None
597
+ intraday: QuoteDetails | None = None
598
+ option: QuoteDetails | None = None
599
+ week52: QuoteDetails | None = Field(default=None, alias="week52")
600
+ mutual_fund: MutualFundQuoteDetails | None = Field(default=None, alias="mutualFund")
601
+ product: Product | None = None
602
+
603
+ @field_validator("date_time", "date_time_utc", mode="before")
604
+ @classmethod
605
+ def parse_quote_datetimes(cls, value: object) -> datetime | None:
606
+ return parse_broker_datetime(value)
607
+
608
+ @model_validator(mode="before")
609
+ @classmethod
610
+ def normalize_nested_aliases(cls, value: object) -> object:
611
+ if not isinstance(value, dict):
612
+ return value
613
+ data = dict(cast(dict[str, Any], value))
614
+ aliases = {
615
+ "All": "all",
616
+ "Fundamental": "fundamental",
617
+ "Intraday": "intraday",
618
+ "Option": "option",
619
+ "Week52": "week52",
620
+ "MutualFund": "mutualFund",
621
+ "Product": "product",
622
+ }
623
+ for source, target in aliases.items():
624
+ if source in data and target not in data:
625
+ data[target] = data[source]
626
+ data.pop(source, None)
627
+ return data
628
+
629
+
630
+ def _empty_quotes() -> list[Quote]:
631
+ return []
632
+
633
+
634
+ class QuotesResponse(BrokerModel):
635
+ messages: QuoteMessages | None = None
636
+ quotes: list[Quote] = Field(default_factory=_empty_quotes, alias="quoteData")
637
+
638
+ @model_validator(mode="before")
639
+ @classmethod
640
+ def normalize_response(cls, value: object) -> object:
641
+ if not isinstance(value, dict):
642
+ return value
643
+ data = dict(cast(dict[str, Any], value))
644
+ if "QuoteData" in data and "quoteData" not in data:
645
+ data["quoteData"] = data["QuoteData"]
646
+ data.pop("QuoteData", None)
647
+ if "Messages" in data and "messages" not in data:
648
+ data["messages"] = data["Messages"]
649
+ data.pop("Messages", None)
650
+ return data
651
+
652
+ @field_validator("quotes", mode="before")
653
+ @classmethod
654
+ def normalize_quotes(cls, value: object) -> object:
655
+ if value is None:
656
+ return []
657
+ if isinstance(value, dict):
658
+ return [cast(dict[str, Any], value)]
659
+ return value
660
+
661
+
662
+ class OptionExpiration(BrokerModel):
663
+ year: int | None = None
664
+ month: int | None = None
665
+ day: int | None = None
666
+ expiry_type: str | None = Field(default=None, alias="expiryType")
667
+
668
+
669
+ def _empty_option_expirations() -> list[OptionExpiration]:
670
+ return []
671
+
672
+
673
+ class OptionExpirationsResponse(BrokerModel):
674
+ expiration_dates: list[OptionExpiration] = Field(
675
+ default_factory=_empty_option_expirations, alias="expirationDates"
676
+ )
677
+
678
+ @model_validator(mode="before")
679
+ @classmethod
680
+ def normalize_response(cls, value: object) -> object:
681
+ if not isinstance(value, dict):
682
+ return value
683
+ data = dict(cast(dict[str, Any], value))
684
+ for source in ("ExpirationDates", "ExpirationDate", "expirationDate"):
685
+ if source in data and "expirationDates" not in data:
686
+ data["expirationDates"] = data[source]
687
+ data.pop(source, None)
688
+ return data
689
+
690
+ @field_validator("expiration_dates", mode="before")
691
+ @classmethod
692
+ def normalize_expiration_dates(cls, value: object) -> object:
693
+ if value is None:
694
+ return []
695
+ if isinstance(value, dict):
696
+ return [cast(dict[str, Any], value)]
697
+ return value
698
+
699
+
700
+ class OptionContract(BrokerModel):
701
+ option_category: str | None = Field(default=None, alias="optionCategory")
702
+ option_root_symbol: str | None = Field(default=None, alias="optionRootSymbol")
703
+ time_stamp: datetime | None = Field(default=None, alias="timeStamp")
704
+ adjusted_flag: bool | str | None = Field(default=None, alias="adjustedFlag")
705
+ display_symbol: str | None = Field(default=None, alias="displaySymbol")
706
+ option_type: str | None = Field(default=None, alias="optionType")
707
+ strike_price: Decimal | None = Field(default=None, alias="strikePrice")
708
+ symbol: str | None = None
709
+ bid: Decimal | None = None
710
+ ask: Decimal | None = None
711
+ bid_size: int | None = Field(default=None, alias="bidSize")
712
+ ask_size: int | None = Field(default=None, alias="askSize")
713
+ in_the_money: str | None = Field(default=None, alias="inTheMoney")
714
+ volume: int | None = None
715
+ open_interest: int | None = Field(default=None, alias="openInterest")
716
+ net_change: Decimal | None = Field(default=None, alias="netChange")
717
+ last_price: Decimal | None = Field(default=None, alias="lastPrice")
718
+ quote_detail: str | None = Field(default=None, alias="quoteDetail")
719
+ osi_key: str | None = Field(default=None, alias="osiKey")
720
+ option_greek: OptionGreeks | None = Field(default=None, alias="optionGreek")
721
+
722
+ @field_validator("time_stamp", mode="before")
723
+ @classmethod
724
+ def parse_time_stamp(cls, value: object) -> datetime | None:
725
+ return parse_broker_datetime(value)
726
+
727
+ @model_validator(mode="before")
728
+ @classmethod
729
+ def normalize_nested_aliases(cls, value: object) -> object:
730
+ if not isinstance(value, dict):
731
+ return value
732
+ data = dict(cast(dict[str, Any], value))
733
+ for source in ("OptionGreeks", "optionGreeks"):
734
+ if source in data and "optionGreek" not in data:
735
+ data["optionGreek"] = data[source]
736
+ data.pop(source, None)
737
+ return data
738
+
739
+
740
+ class OptionChainPair(BrokerModel):
741
+ call: OptionContract | None = None
742
+ put: OptionContract | None = None
743
+ pair_type: str | None = Field(default=None, alias="pairType")
744
+
745
+ @model_validator(mode="before")
746
+ @classmethod
747
+ def normalize_nested_aliases(cls, value: object) -> object:
748
+ if not isinstance(value, dict):
749
+ return value
750
+ data = dict(cast(dict[str, Any], value))
751
+ for source, target in {
752
+ "Call": "call",
753
+ "OptionCall": "call",
754
+ "optioncall": "call",
755
+ "Put": "put",
756
+ "OptionPut": "put",
757
+ "optionPut": "put",
758
+ }.items():
759
+ if source in data and target not in data:
760
+ data[target] = data[source]
761
+ data.pop(source, None)
762
+ return data
763
+
764
+
765
+ class SelectedExpiration(BrokerModel):
766
+ month: int | None = None
767
+ year: int | None = None
768
+ day: int | None = None
769
+
770
+
771
+ def _empty_option_pairs() -> list[OptionChainPair]:
772
+ return []
773
+
774
+
775
+ class OptionChainResponse(BrokerModel):
776
+ option_pairs: list[OptionChainPair] = Field(
777
+ default_factory=_empty_option_pairs, alias="optionPairs"
778
+ )
779
+ time_stamp: datetime | None = Field(default=None, alias="timeStamp")
780
+ quote_type: str | None = Field(default=None, alias="quoteType")
781
+ near_price: Decimal | None = Field(default=None, alias="nearPrice")
782
+ selected: SelectedExpiration | None = None
783
+
784
+ @field_validator("time_stamp", mode="before")
785
+ @classmethod
786
+ def parse_time_stamp(cls, value: object) -> datetime | None:
787
+ return parse_broker_datetime(value)
788
+
789
+ @model_validator(mode="before")
790
+ @classmethod
791
+ def normalize_response(cls, value: object) -> object:
792
+ if not isinstance(value, dict):
793
+ return value
794
+ data = dict(cast(dict[str, Any], value))
795
+ for source in ("OptionPairs", "OptionPair", "optionPair"):
796
+ if source in data and "optionPairs" not in data:
797
+ data["optionPairs"] = data[source]
798
+ data.pop(source, None)
799
+ if "SelectedED" in data and "selected" not in data:
800
+ data["selected"] = data["SelectedED"]
801
+ data.pop("SelectedED", None)
802
+ return data
803
+
804
+ @field_validator("option_pairs", mode="before")
805
+ @classmethod
806
+ def normalize_option_pairs(cls, value: object) -> object:
807
+ if value is None:
808
+ return []
809
+ if isinstance(value, dict):
810
+ return [cast(dict[str, Any], value)]
811
+ return value