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,617 @@
|
|
|
1
|
+
"""Fluent helpers for constructing E*TRADE order requests."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping, Sequence
|
|
6
|
+
from datetime import date
|
|
7
|
+
from decimal import Decimal
|
|
8
|
+
from typing import Any, Literal, TypeAlias, cast
|
|
9
|
+
|
|
10
|
+
from etrade_python.orders.models import (
|
|
11
|
+
Disclosure,
|
|
12
|
+
OrderInstrumentRequest,
|
|
13
|
+
PlaceOrderRequest,
|
|
14
|
+
PreviewId,
|
|
15
|
+
PreviewOrderRequest,
|
|
16
|
+
)
|
|
17
|
+
from etrade_python.orders.values import (
|
|
18
|
+
VALID_MARKET_SESSIONS,
|
|
19
|
+
VALID_ORDER_ACTIONS,
|
|
20
|
+
VALID_ORDER_TERMS,
|
|
21
|
+
VALID_ORDER_TYPES,
|
|
22
|
+
VALID_PRICE_TYPES,
|
|
23
|
+
VALID_QUANTITY_TYPES,
|
|
24
|
+
VALID_SECURITY_TYPES,
|
|
25
|
+
MarketSession,
|
|
26
|
+
OrderAction,
|
|
27
|
+
OrderTerm,
|
|
28
|
+
OrderType,
|
|
29
|
+
PriceType,
|
|
30
|
+
QuantityType,
|
|
31
|
+
SecurityType,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"QuantityType",
|
|
36
|
+
"OrderTerm",
|
|
37
|
+
"OrderType",
|
|
38
|
+
"PriceType",
|
|
39
|
+
"MarketSession",
|
|
40
|
+
"OrderAction",
|
|
41
|
+
"SecurityType",
|
|
42
|
+
"VALID_QUANTITY_TYPES",
|
|
43
|
+
"VALID_ORDER_TERMS",
|
|
44
|
+
"VALID_ORDER_TYPES",
|
|
45
|
+
"VALID_PRICE_TYPES",
|
|
46
|
+
"VALID_MARKET_SESSIONS",
|
|
47
|
+
"VALID_ORDER_ACTIONS",
|
|
48
|
+
"VALID_SECURITY_TYPES",
|
|
49
|
+
"OrderBuilder",
|
|
50
|
+
"NumberLike",
|
|
51
|
+
"PreviewIdInput",
|
|
52
|
+
"InstrumentInput",
|
|
53
|
+
"DisclosureInput",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
NumberLike: TypeAlias = Decimal | int | str | float
|
|
58
|
+
PreviewIdInput: TypeAlias = int | dict[str, int] | PreviewId
|
|
59
|
+
InstrumentInput: TypeAlias = OrderInstrumentRequest | Mapping[str, Any]
|
|
60
|
+
DisclosureInput: TypeAlias = Disclosure | Mapping[str, Any]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class OrderBuilder:
|
|
64
|
+
"""Build typed E*TRADE preview and place order requests."""
|
|
65
|
+
|
|
66
|
+
def __init__(self, account_id_key: str) -> None:
|
|
67
|
+
self._account_id_key = _nonempty(account_id_key, "account_id_key")
|
|
68
|
+
self._order_id: int | None = None
|
|
69
|
+
self._order_type: OrderType | None = None
|
|
70
|
+
self._client_order_id: str | None = None
|
|
71
|
+
self._symbol: str | None = None
|
|
72
|
+
self._expiration: date | None = None
|
|
73
|
+
self._quantity_type: QuantityType | None = None
|
|
74
|
+
self._order_term: OrderTerm = "GOOD_FOR_DAY"
|
|
75
|
+
self._price_type: PriceType | None = None
|
|
76
|
+
self._limit_price: Decimal | None = None
|
|
77
|
+
self._stop_price: Decimal | str | None = None
|
|
78
|
+
self._stop_limit_price: Decimal | None = None
|
|
79
|
+
self._market_session: MarketSession = "REGULAR"
|
|
80
|
+
self._all_or_none: bool | str | None = "false"
|
|
81
|
+
self._disclosure: Disclosure | None = None
|
|
82
|
+
self._detail_fields: dict[str, Any] = {}
|
|
83
|
+
self._instruments: list[OrderInstrumentRequest] = []
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def for_account(cls, account_id_key: str) -> OrderBuilder:
|
|
87
|
+
return cls(account_id_key)
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def account_id_key(self) -> str:
|
|
91
|
+
return self._account_id_key
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def order_id_value(self) -> int | None:
|
|
95
|
+
return self._order_id
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def order_id_for_change_value(self) -> int | None:
|
|
99
|
+
return self._order_id
|
|
100
|
+
|
|
101
|
+
def client_order_id(self, client_order_id: str) -> OrderBuilder:
|
|
102
|
+
self._client_order_id = _nonempty(client_order_id, "client_order_id")
|
|
103
|
+
return self
|
|
104
|
+
|
|
105
|
+
def order_id(self, order_id: int) -> OrderBuilder:
|
|
106
|
+
if type(order_id) is not int or order_id < 1:
|
|
107
|
+
raise ValueError("order_id must be a positive integer")
|
|
108
|
+
self._order_id = order_id
|
|
109
|
+
return self
|
|
110
|
+
|
|
111
|
+
def order_id_for_change(self, order_id: int) -> OrderBuilder:
|
|
112
|
+
return self.order_id(order_id)
|
|
113
|
+
|
|
114
|
+
def order_type(self, order_type: OrderType | str) -> OrderBuilder:
|
|
115
|
+
self._order_type = cast(OrderType, _valid_enum(order_type, VALID_ORDER_TYPES, "order_type"))
|
|
116
|
+
return self
|
|
117
|
+
|
|
118
|
+
def with_symbol(self, symbol: str) -> OrderBuilder:
|
|
119
|
+
self._symbol = _upper(symbol, "symbol")
|
|
120
|
+
return self
|
|
121
|
+
|
|
122
|
+
def with_expiry(self, year: int, month: int, day: int) -> OrderBuilder:
|
|
123
|
+
try:
|
|
124
|
+
self._expiration = date(year, month, day)
|
|
125
|
+
except ValueError as exc:
|
|
126
|
+
raise ValueError("expiry date must be a valid calendar date") from exc
|
|
127
|
+
return self
|
|
128
|
+
|
|
129
|
+
def with_expiration(self, expiration: date) -> OrderBuilder:
|
|
130
|
+
self._expiration = expiration
|
|
131
|
+
return self
|
|
132
|
+
|
|
133
|
+
def quantity_type(self, quantity_type: QuantityType | str) -> OrderBuilder:
|
|
134
|
+
self._quantity_type = cast(
|
|
135
|
+
QuantityType, _valid_enum(quantity_type, VALID_QUANTITY_TYPES, "quantity_type")
|
|
136
|
+
)
|
|
137
|
+
return self
|
|
138
|
+
|
|
139
|
+
def term(self, order_term: OrderTerm | str) -> OrderBuilder:
|
|
140
|
+
self._order_term = cast(OrderTerm, _valid_enum(order_term, VALID_ORDER_TERMS, "order_term"))
|
|
141
|
+
return self
|
|
142
|
+
|
|
143
|
+
def gfd(self) -> OrderBuilder:
|
|
144
|
+
return self.term("GOOD_FOR_DAY")
|
|
145
|
+
|
|
146
|
+
def gtc(self) -> OrderBuilder:
|
|
147
|
+
return self.term("GOOD_UNTIL_CANCEL")
|
|
148
|
+
|
|
149
|
+
def price_type(self, price_type: PriceType | str) -> OrderBuilder:
|
|
150
|
+
self._price_type = cast(PriceType, _valid_enum(price_type, VALID_PRICE_TYPES, "price_type"))
|
|
151
|
+
return self
|
|
152
|
+
|
|
153
|
+
def market(self) -> OrderBuilder:
|
|
154
|
+
self._price_type = "MARKET"
|
|
155
|
+
self._limit_price = None
|
|
156
|
+
self._stop_price = None
|
|
157
|
+
self._stop_limit_price = None
|
|
158
|
+
return self
|
|
159
|
+
|
|
160
|
+
def limit(self, limit_price: NumberLike) -> OrderBuilder:
|
|
161
|
+
return self.limit_price(limit_price)
|
|
162
|
+
|
|
163
|
+
def limit_price(self, limit_price: NumberLike) -> OrderBuilder:
|
|
164
|
+
self._price_type = "LIMIT"
|
|
165
|
+
self._limit_price = _positive_decimal(limit_price, "limit_price")
|
|
166
|
+
return self
|
|
167
|
+
|
|
168
|
+
def net_debit(self, limit_price: NumberLike) -> OrderBuilder:
|
|
169
|
+
self._price_type = "NET_DEBIT"
|
|
170
|
+
self._limit_price = _positive_decimal(limit_price, "limit_price")
|
|
171
|
+
return self
|
|
172
|
+
|
|
173
|
+
def net_credit(self, limit_price: NumberLike) -> OrderBuilder:
|
|
174
|
+
self._price_type = "NET_CREDIT"
|
|
175
|
+
self._limit_price = _positive_decimal(limit_price, "limit_price")
|
|
176
|
+
return self
|
|
177
|
+
|
|
178
|
+
def stop(self, stop_price: NumberLike) -> OrderBuilder:
|
|
179
|
+
return self.stop_price(stop_price)
|
|
180
|
+
|
|
181
|
+
def stop_price(self, stop_price: NumberLike) -> OrderBuilder:
|
|
182
|
+
self._price_type = "STOP"
|
|
183
|
+
self._stop_price = _positive_decimal(stop_price, "stop_price")
|
|
184
|
+
self._limit_price = None
|
|
185
|
+
return self
|
|
186
|
+
|
|
187
|
+
def stop_limit(self, stop_price: NumberLike, limit_price: NumberLike) -> OrderBuilder:
|
|
188
|
+
self._price_type = "STOP_LIMIT"
|
|
189
|
+
self._stop_price = _positive_decimal(stop_price, "stop_price")
|
|
190
|
+
self._stop_limit_price = _positive_decimal(limit_price, "stop_limit_price")
|
|
191
|
+
self._limit_price = self._stop_limit_price
|
|
192
|
+
return self
|
|
193
|
+
|
|
194
|
+
def stop_limit_price(self, stop_limit_price: NumberLike) -> OrderBuilder:
|
|
195
|
+
self._price_type = "STOP_LIMIT"
|
|
196
|
+
self._stop_limit_price = _positive_decimal(stop_limit_price, "stop_limit_price")
|
|
197
|
+
self._limit_price = self._stop_limit_price
|
|
198
|
+
return self
|
|
199
|
+
|
|
200
|
+
def market_session(self, market_session: MarketSession | str) -> OrderBuilder:
|
|
201
|
+
self._market_session = cast(
|
|
202
|
+
MarketSession, _valid_enum(market_session, VALID_MARKET_SESSIONS, "market_session")
|
|
203
|
+
)
|
|
204
|
+
return self
|
|
205
|
+
|
|
206
|
+
def all_or_none(self, all_or_none: bool) -> OrderBuilder:
|
|
207
|
+
self._all_or_none = all_or_none
|
|
208
|
+
return self
|
|
209
|
+
|
|
210
|
+
def disclosure(self, disclosure: DisclosureInput) -> OrderBuilder:
|
|
211
|
+
if isinstance(disclosure, Disclosure):
|
|
212
|
+
self._disclosure = disclosure
|
|
213
|
+
else:
|
|
214
|
+
self._disclosure = Disclosure.model_validate(dict(disclosure))
|
|
215
|
+
return self
|
|
216
|
+
|
|
217
|
+
def with_detail(self, fields: Mapping[str, Any]) -> OrderBuilder:
|
|
218
|
+
for key, value in fields.items():
|
|
219
|
+
if key not in {"instrument", "Instrument"}:
|
|
220
|
+
self._detail_fields[key] = value
|
|
221
|
+
return self
|
|
222
|
+
|
|
223
|
+
def add_instrument(self, instrument: InstrumentInput) -> OrderBuilder:
|
|
224
|
+
if isinstance(instrument, OrderInstrumentRequest):
|
|
225
|
+
model = instrument
|
|
226
|
+
else:
|
|
227
|
+
data = dict(instrument)
|
|
228
|
+
if data.get("quantityType") is None and data.get("quantity_type") is None:
|
|
229
|
+
data["quantityType"] = self._quantity_type
|
|
230
|
+
elif data.get("quantityType") is not None:
|
|
231
|
+
data["quantityType"] = _valid_enum(
|
|
232
|
+
str(data["quantityType"]), VALID_QUANTITY_TYPES, "quantity_type"
|
|
233
|
+
)
|
|
234
|
+
elif data.get("quantity_type") is not None:
|
|
235
|
+
data["quantity_type"] = _valid_enum(
|
|
236
|
+
str(data["quantity_type"]), VALID_QUANTITY_TYPES, "quantity_type"
|
|
237
|
+
)
|
|
238
|
+
model = OrderInstrumentRequest.model_validate(data)
|
|
239
|
+
self._instruments.append(model)
|
|
240
|
+
return self
|
|
241
|
+
|
|
242
|
+
def add_equity(
|
|
243
|
+
self,
|
|
244
|
+
order_action: OrderAction | str,
|
|
245
|
+
quantity: NumberLike = Decimal("1"),
|
|
246
|
+
*,
|
|
247
|
+
symbol: str | None = None,
|
|
248
|
+
overrides: Mapping[str, Any] | None = None,
|
|
249
|
+
) -> OrderBuilder:
|
|
250
|
+
overrides = dict(overrides or {})
|
|
251
|
+
action = cast(OrderAction, _valid_enum(order_action, VALID_ORDER_ACTIONS, "order_action"))
|
|
252
|
+
symbol_value = _symbol_from_override(overrides, symbol, self._symbol, "equity")
|
|
253
|
+
security_type = cast(
|
|
254
|
+
SecurityType,
|
|
255
|
+
_valid_enum(
|
|
256
|
+
str(overrides.get("securityType", "EQ")), VALID_SECURITY_TYPES, "security_type"
|
|
257
|
+
),
|
|
258
|
+
)
|
|
259
|
+
data: dict[str, Any] = {
|
|
260
|
+
"orderAction": action,
|
|
261
|
+
"quantityType": overrides.get("quantityType", self._quantity_type),
|
|
262
|
+
"quantity": _positive_decimal(quantity, "quantity"),
|
|
263
|
+
"orderedQuantity": _optional_decimal(
|
|
264
|
+
overrides.get("orderedQuantity"), "ordered_quantity"
|
|
265
|
+
),
|
|
266
|
+
"Product": {"symbol": symbol_value, "securityType": security_type},
|
|
267
|
+
}
|
|
268
|
+
return self.add_instrument(_drop_none(data))
|
|
269
|
+
|
|
270
|
+
def add_long_call(
|
|
271
|
+
self,
|
|
272
|
+
strike_price: NumberLike,
|
|
273
|
+
quantity: NumberLike = Decimal("1"),
|
|
274
|
+
overrides: Mapping[str, Any] | None = None,
|
|
275
|
+
) -> OrderBuilder:
|
|
276
|
+
return self._add_option_leg("CALL", "BUY_OPEN", strike_price, quantity, overrides)
|
|
277
|
+
|
|
278
|
+
def add_short_call(
|
|
279
|
+
self,
|
|
280
|
+
strike_price: NumberLike,
|
|
281
|
+
quantity: NumberLike = Decimal("1"),
|
|
282
|
+
overrides: Mapping[str, Any] | None = None,
|
|
283
|
+
) -> OrderBuilder:
|
|
284
|
+
return self._add_option_leg("CALL", "SELL_OPEN", strike_price, quantity, overrides)
|
|
285
|
+
|
|
286
|
+
def add_long_put(
|
|
287
|
+
self,
|
|
288
|
+
strike_price: NumberLike,
|
|
289
|
+
quantity: NumberLike = Decimal("1"),
|
|
290
|
+
overrides: Mapping[str, Any] | None = None,
|
|
291
|
+
) -> OrderBuilder:
|
|
292
|
+
return self._add_option_leg("PUT", "BUY_OPEN", strike_price, quantity, overrides)
|
|
293
|
+
|
|
294
|
+
def add_short_put(
|
|
295
|
+
self,
|
|
296
|
+
strike_price: NumberLike,
|
|
297
|
+
quantity: NumberLike = Decimal("1"),
|
|
298
|
+
overrides: Mapping[str, Any] | None = None,
|
|
299
|
+
) -> OrderBuilder:
|
|
300
|
+
return self._add_option_leg("PUT", "SELL_OPEN", strike_price, quantity, overrides)
|
|
301
|
+
|
|
302
|
+
def equity_limit(
|
|
303
|
+
self,
|
|
304
|
+
symbol: str,
|
|
305
|
+
*,
|
|
306
|
+
action: OrderAction | str,
|
|
307
|
+
quantity: NumberLike,
|
|
308
|
+
limit_price: NumberLike,
|
|
309
|
+
) -> OrderBuilder:
|
|
310
|
+
return (
|
|
311
|
+
self.order_type("EQ")
|
|
312
|
+
.with_symbol(symbol)
|
|
313
|
+
.limit_price(limit_price)
|
|
314
|
+
.add_equity(action, quantity)
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
def long_call_limit(
|
|
318
|
+
self,
|
|
319
|
+
symbol: str,
|
|
320
|
+
*,
|
|
321
|
+
expiration: date,
|
|
322
|
+
strike_price: NumberLike,
|
|
323
|
+
quantity: NumberLike,
|
|
324
|
+
limit_price: NumberLike,
|
|
325
|
+
) -> OrderBuilder:
|
|
326
|
+
return (
|
|
327
|
+
self.order_type("OPTN")
|
|
328
|
+
.with_symbol(symbol)
|
|
329
|
+
.with_expiration(expiration)
|
|
330
|
+
.limit_price(limit_price)
|
|
331
|
+
.add_long_call(strike_price, quantity)
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
def option_vertical_call(
|
|
335
|
+
self,
|
|
336
|
+
symbol: str,
|
|
337
|
+
*,
|
|
338
|
+
expiration: date,
|
|
339
|
+
quantity: NumberLike,
|
|
340
|
+
long_strike: NumberLike,
|
|
341
|
+
short_strike: NumberLike,
|
|
342
|
+
net_debit: NumberLike,
|
|
343
|
+
) -> OrderBuilder:
|
|
344
|
+
return (
|
|
345
|
+
self.order_type("SPREADS")
|
|
346
|
+
.with_symbol(symbol)
|
|
347
|
+
.with_expiration(expiration)
|
|
348
|
+
.net_debit(net_debit)
|
|
349
|
+
.add_long_call(long_strike, quantity)
|
|
350
|
+
.add_short_call(short_strike, quantity)
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
def three_leg_call_spread_short_put(
|
|
354
|
+
self,
|
|
355
|
+
symbol: str,
|
|
356
|
+
*,
|
|
357
|
+
expiration: date,
|
|
358
|
+
quantity: NumberLike,
|
|
359
|
+
long_call_strike: NumberLike,
|
|
360
|
+
short_call_strike: NumberLike,
|
|
361
|
+
short_put_strike: NumberLike,
|
|
362
|
+
net_debit: NumberLike,
|
|
363
|
+
) -> OrderBuilder:
|
|
364
|
+
return (
|
|
365
|
+
self.order_type("SPREADS")
|
|
366
|
+
.with_symbol(symbol)
|
|
367
|
+
.with_expiration(expiration)
|
|
368
|
+
.net_debit(net_debit)
|
|
369
|
+
.add_long_call(long_call_strike, quantity)
|
|
370
|
+
.add_short_call(short_call_strike, quantity)
|
|
371
|
+
.add_short_put(short_put_strike, quantity)
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
def iron_condor(
|
|
375
|
+
self,
|
|
376
|
+
symbol: str,
|
|
377
|
+
*,
|
|
378
|
+
expiration: date,
|
|
379
|
+
quantity: NumberLike,
|
|
380
|
+
short_put_strike: NumberLike,
|
|
381
|
+
long_put_strike: NumberLike,
|
|
382
|
+
short_call_strike: NumberLike,
|
|
383
|
+
long_call_strike: NumberLike,
|
|
384
|
+
net_credit: NumberLike,
|
|
385
|
+
) -> OrderBuilder:
|
|
386
|
+
return (
|
|
387
|
+
self.order_type("SPREADS")
|
|
388
|
+
.with_symbol(symbol)
|
|
389
|
+
.with_expiration(expiration)
|
|
390
|
+
.net_credit(net_credit)
|
|
391
|
+
.add_short_put(short_put_strike, quantity)
|
|
392
|
+
.add_long_put(long_put_strike, quantity)
|
|
393
|
+
.add_short_call(short_call_strike, quantity)
|
|
394
|
+
.add_long_call(long_call_strike, quantity)
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
def buy_write(
|
|
398
|
+
self,
|
|
399
|
+
symbol: str,
|
|
400
|
+
*,
|
|
401
|
+
expiration: date,
|
|
402
|
+
stock_quantity: NumberLike,
|
|
403
|
+
call_quantity: NumberLike,
|
|
404
|
+
call_strike: NumberLike,
|
|
405
|
+
net_debit: NumberLike,
|
|
406
|
+
) -> OrderBuilder:
|
|
407
|
+
return (
|
|
408
|
+
self.order_type("BUY_WRITES")
|
|
409
|
+
.with_symbol(symbol)
|
|
410
|
+
.net_debit(net_debit)
|
|
411
|
+
.add_equity("BUY", stock_quantity)
|
|
412
|
+
.with_expiration(expiration)
|
|
413
|
+
.add_short_call(call_strike, call_quantity)
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
def build_preview_request(self) -> PreviewOrderRequest:
|
|
417
|
+
self._assert_ready_for_preview()
|
|
418
|
+
return PreviewOrderRequest.model_validate(
|
|
419
|
+
{
|
|
420
|
+
"orderType": self._order_type,
|
|
421
|
+
"clientOrderId": self._client_order_id,
|
|
422
|
+
"Order": [self._build_order_detail_payload()],
|
|
423
|
+
}
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
def build_change_preview_request(self, order_id: int | None = None) -> PreviewOrderRequest:
|
|
427
|
+
if order_id is not None:
|
|
428
|
+
self.order_id(order_id)
|
|
429
|
+
if self._order_id is None:
|
|
430
|
+
raise ValueError("order_id is required to preview an order change")
|
|
431
|
+
return self.build_preview_request()
|
|
432
|
+
|
|
433
|
+
def build_place_request(self, preview_ids: Sequence[PreviewIdInput]) -> PlaceOrderRequest:
|
|
434
|
+
self._assert_ready_for_preview()
|
|
435
|
+
normalized_ids = _normalize_preview_ids(preview_ids)
|
|
436
|
+
return PlaceOrderRequest.model_validate(
|
|
437
|
+
{
|
|
438
|
+
"orderType": self._order_type,
|
|
439
|
+
"clientOrderId": self._client_order_id,
|
|
440
|
+
"PreviewIds": normalized_ids,
|
|
441
|
+
"Order": [self._build_order_detail_payload()],
|
|
442
|
+
}
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
def build_change_place_request(
|
|
446
|
+
self, preview_ids: Sequence[PreviewIdInput], order_id: int | None = None
|
|
447
|
+
) -> PlaceOrderRequest:
|
|
448
|
+
if order_id is not None:
|
|
449
|
+
self.order_id(order_id)
|
|
450
|
+
if self._order_id is None:
|
|
451
|
+
raise ValueError("order_id is required to place an order change")
|
|
452
|
+
return self.build_place_request(preview_ids)
|
|
453
|
+
|
|
454
|
+
def _add_option_leg(
|
|
455
|
+
self,
|
|
456
|
+
call_put: Literal["CALL", "PUT"],
|
|
457
|
+
order_action: OrderAction,
|
|
458
|
+
strike_price: NumberLike,
|
|
459
|
+
quantity: NumberLike,
|
|
460
|
+
overrides: Mapping[str, Any] | None,
|
|
461
|
+
) -> OrderBuilder:
|
|
462
|
+
overrides = dict(overrides or {})
|
|
463
|
+
symbol = _symbol_from_override(overrides, None, self._symbol, "option")
|
|
464
|
+
expiry = _expiry_from_override(overrides, self._expiration)
|
|
465
|
+
security_type = cast(
|
|
466
|
+
SecurityType,
|
|
467
|
+
_valid_enum(
|
|
468
|
+
str(overrides.get("securityType", "OPTN")), VALID_SECURITY_TYPES, "security_type"
|
|
469
|
+
),
|
|
470
|
+
)
|
|
471
|
+
data: dict[str, Any] = {
|
|
472
|
+
"orderAction": order_action,
|
|
473
|
+
"quantityType": overrides.get("quantityType", self._quantity_type),
|
|
474
|
+
"quantity": _positive_decimal(quantity, "quantity"),
|
|
475
|
+
"orderedQuantity": _optional_decimal(
|
|
476
|
+
overrides.get("orderedQuantity", quantity), "ordered_quantity"
|
|
477
|
+
),
|
|
478
|
+
"Product": {
|
|
479
|
+
"symbol": symbol,
|
|
480
|
+
"securityType": security_type,
|
|
481
|
+
"callPut": call_put,
|
|
482
|
+
"expiryYear": expiry.year,
|
|
483
|
+
"expiryMonth": expiry.month,
|
|
484
|
+
"expiryDay": expiry.day,
|
|
485
|
+
"strikePrice": _positive_decimal(strike_price, "strike_price"),
|
|
486
|
+
},
|
|
487
|
+
}
|
|
488
|
+
return self.add_instrument(_drop_none(data))
|
|
489
|
+
|
|
490
|
+
def _build_order_detail_payload(self) -> dict[str, Any]:
|
|
491
|
+
payload: dict[str, Any] = {
|
|
492
|
+
"allOrNone": self._all_or_none,
|
|
493
|
+
"priceType": self._price_type,
|
|
494
|
+
"orderTerm": self._order_term,
|
|
495
|
+
"marketSession": self._market_session,
|
|
496
|
+
"stopPrice": self._stop_price,
|
|
497
|
+
"limitPrice": self._limit_price,
|
|
498
|
+
"stopLimitPrice": self._stop_limit_price,
|
|
499
|
+
"disclosure": self._disclosure,
|
|
500
|
+
}
|
|
501
|
+
payload.update(self._detail_fields)
|
|
502
|
+
payload.pop("instrument", None)
|
|
503
|
+
payload.pop("Instrument", None)
|
|
504
|
+
payload["Instrument"] = list(self._instruments)
|
|
505
|
+
if payload.get("stopPrice") == "":
|
|
506
|
+
payload["stopPrice"] = None
|
|
507
|
+
return _drop_none(payload)
|
|
508
|
+
|
|
509
|
+
def _assert_ready_for_preview(self) -> None:
|
|
510
|
+
if self._client_order_id is None:
|
|
511
|
+
raise ValueError("client_order_id is required")
|
|
512
|
+
if self._order_type is None:
|
|
513
|
+
raise ValueError("order_type is required")
|
|
514
|
+
if self._price_type is None:
|
|
515
|
+
raise ValueError("price_type is required")
|
|
516
|
+
if not self._instruments:
|
|
517
|
+
raise ValueError("At least one instrument is required")
|
|
518
|
+
|
|
519
|
+
def _require_symbol(self) -> str:
|
|
520
|
+
if self._symbol is None:
|
|
521
|
+
raise ValueError("symbol is required")
|
|
522
|
+
return self._symbol
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _normalize_preview_ids(preview_ids: Sequence[PreviewIdInput]) -> list[PreviewId]:
|
|
526
|
+
if not preview_ids:
|
|
527
|
+
raise ValueError("At least one preview ID is required")
|
|
528
|
+
normalized: list[PreviewId] = []
|
|
529
|
+
for preview_id in preview_ids:
|
|
530
|
+
if isinstance(preview_id, PreviewId):
|
|
531
|
+
PreviewId.model_validate({"previewId": preview_id.preview_id})
|
|
532
|
+
normalized.append(preview_id)
|
|
533
|
+
continue
|
|
534
|
+
if isinstance(preview_id, bool):
|
|
535
|
+
raise ValueError("preview IDs must be positive integers")
|
|
536
|
+
if isinstance(preview_id, int):
|
|
537
|
+
if preview_id < 1:
|
|
538
|
+
raise ValueError("preview IDs must be positive integers")
|
|
539
|
+
normalized.append(PreviewId(previewId=preview_id))
|
|
540
|
+
continue
|
|
541
|
+
if not isinstance(cast(object, preview_id), Mapping):
|
|
542
|
+
raise ValueError("preview IDs must be positive integers")
|
|
543
|
+
value = preview_id.get("previewId")
|
|
544
|
+
if isinstance(value, bool) or not isinstance(value, int) or value < 1:
|
|
545
|
+
raise ValueError("preview IDs must be positive integers")
|
|
546
|
+
normalized.append(PreviewId(previewId=value))
|
|
547
|
+
return normalized
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def _symbol_from_override(
|
|
551
|
+
overrides: Mapping[str, Any], symbol: str | None, default_symbol: str | None, leg_type: str
|
|
552
|
+
) -> str:
|
|
553
|
+
raw_symbol = overrides.get("symbol", symbol if symbol is not None else default_symbol)
|
|
554
|
+
if raw_symbol is None:
|
|
555
|
+
raise ValueError(f"symbol is required for {leg_type} legs")
|
|
556
|
+
return _upper(str(raw_symbol), "symbol")
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def _expiry_from_override(overrides: Mapping[str, Any], default_expiration: date | None) -> date:
|
|
560
|
+
if all(key in overrides for key in ("expiryYear", "expiryMonth", "expiryDay")):
|
|
561
|
+
try:
|
|
562
|
+
return date(
|
|
563
|
+
int(overrides["expiryYear"]),
|
|
564
|
+
int(overrides["expiryMonth"]),
|
|
565
|
+
int(overrides["expiryDay"]),
|
|
566
|
+
)
|
|
567
|
+
except (TypeError, ValueError) as exc:
|
|
568
|
+
raise ValueError("expiry date must be a valid calendar date") from exc
|
|
569
|
+
if default_expiration is None:
|
|
570
|
+
raise ValueError("expiration is required for option legs")
|
|
571
|
+
return default_expiration
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def _valid_enum(value: str, allowed: frozenset[str], label: str) -> str:
|
|
575
|
+
normalized = _upper(value, label)
|
|
576
|
+
if normalized not in allowed:
|
|
577
|
+
allowed_values = ", ".join(sorted(allowed))
|
|
578
|
+
raise ValueError(f"{label} must be one of: {allowed_values}")
|
|
579
|
+
return normalized
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def _decimal(value: NumberLike, label: str) -> Decimal:
|
|
583
|
+
try:
|
|
584
|
+
decimal = Decimal(str(value))
|
|
585
|
+
except Exception as exc: # noqa: BLE001 - convert arbitrary construction failures
|
|
586
|
+
raise ValueError(f"{label} must be a decimal value") from exc
|
|
587
|
+
if not decimal.is_finite():
|
|
588
|
+
raise ValueError(f"{label} must be finite")
|
|
589
|
+
return decimal
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def _optional_decimal(value: object, label: str) -> Decimal | None:
|
|
593
|
+
if value is None:
|
|
594
|
+
return None
|
|
595
|
+
return _decimal(cast(NumberLike, value), label)
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def _positive_decimal(value: NumberLike, label: str) -> Decimal:
|
|
599
|
+
decimal = _decimal(value, label)
|
|
600
|
+
if decimal <= 0:
|
|
601
|
+
raise ValueError(f"{label} must be positive")
|
|
602
|
+
return decimal
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def _nonempty(value: str, label: str) -> str:
|
|
606
|
+
normalized = value.strip()
|
|
607
|
+
if not normalized:
|
|
608
|
+
raise ValueError(f"{label} is required")
|
|
609
|
+
return normalized
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def _upper(value: str, label: str) -> str:
|
|
613
|
+
return _nonempty(value, label).upper()
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def _drop_none(data: Mapping[str, Any]) -> dict[str, Any]:
|
|
617
|
+
return {key: value for key, value in data.items() if value is not None}
|