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,1009 @@
|
|
|
1
|
+
"""Typed order request and response models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from datetime import date, datetime
|
|
7
|
+
from decimal import Decimal
|
|
8
|
+
from typing import Any, cast
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, ConfigDict, Field, ValidationInfo, field_validator, model_validator
|
|
11
|
+
|
|
12
|
+
from etrade_python._dates import parse_broker_datetime
|
|
13
|
+
from etrade_python.orders.values import (
|
|
14
|
+
VALID_MARKET_SESSIONS,
|
|
15
|
+
VALID_ORDER_ACTIONS,
|
|
16
|
+
VALID_ORDER_TERMS,
|
|
17
|
+
VALID_ORDER_TYPES,
|
|
18
|
+
VALID_PRICE_TYPES,
|
|
19
|
+
VALID_QUANTITY_TYPES,
|
|
20
|
+
VALID_SECURITY_TYPES,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
_CLIENT_ORDER_ID_RE = re.compile(r"^[A-Za-z0-9]{1,20}$")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _supported_value(value: str, allowed: frozenset[str], label: str) -> str:
|
|
27
|
+
normalized = value.strip().upper()
|
|
28
|
+
if normalized not in allowed:
|
|
29
|
+
raise ValueError(f"{label} must be one of: {', '.join(sorted(allowed))}")
|
|
30
|
+
return normalized
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class BrokerModel(BaseModel):
|
|
34
|
+
"""Base response model that discards unmodeled broker fields."""
|
|
35
|
+
|
|
36
|
+
model_config = ConfigDict(extra="ignore", frozen=True, populate_by_name=True)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _strip_broker_metadata(value: object) -> object:
|
|
40
|
+
if isinstance(value, dict):
|
|
41
|
+
return {
|
|
42
|
+
key: _strip_broker_metadata(item)
|
|
43
|
+
for key, item in cast(dict[str, object], value).items()
|
|
44
|
+
if key != "broker_metadata"
|
|
45
|
+
}
|
|
46
|
+
if isinstance(value, list):
|
|
47
|
+
return [_strip_broker_metadata(item) for item in cast(list[object], value)]
|
|
48
|
+
return value
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _normalize_aliases(value: object, aliases: dict[str, str]) -> object:
|
|
52
|
+
if not isinstance(value, dict):
|
|
53
|
+
return value
|
|
54
|
+
data = dict(cast(dict[str, Any], value))
|
|
55
|
+
for source, target in aliases.items():
|
|
56
|
+
if source in data and target not in data:
|
|
57
|
+
data[target] = data[source]
|
|
58
|
+
data.pop(source, None)
|
|
59
|
+
return data
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _normalize_response_ids(value: object, key: str, wrapper: str) -> object:
|
|
63
|
+
if isinstance(value, BaseModel):
|
|
64
|
+
value = value.model_dump(by_alias=True)
|
|
65
|
+
if isinstance(value, dict):
|
|
66
|
+
data = cast(dict[str, Any], value)
|
|
67
|
+
if wrapper in data:
|
|
68
|
+
value = data[wrapper]
|
|
69
|
+
elif isinstance(data.get(key), (dict, list)):
|
|
70
|
+
value = data[key]
|
|
71
|
+
if value is None or value == {}:
|
|
72
|
+
return []
|
|
73
|
+
if isinstance(value, int):
|
|
74
|
+
return [{key: value}]
|
|
75
|
+
if isinstance(value, dict):
|
|
76
|
+
return [cast(dict[str, Any], value)]
|
|
77
|
+
if isinstance(value, list):
|
|
78
|
+
items: list[object] = []
|
|
79
|
+
for item in cast(list[object], value):
|
|
80
|
+
if isinstance(item, int):
|
|
81
|
+
items.append({key: item})
|
|
82
|
+
elif isinstance(item, BaseModel):
|
|
83
|
+
items.append(item.model_dump(by_alias=True))
|
|
84
|
+
else:
|
|
85
|
+
items.append(item)
|
|
86
|
+
return items
|
|
87
|
+
return value
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class OrderProductId(BrokerModel):
|
|
91
|
+
symbol: str | None = None
|
|
92
|
+
type_code: str | None = Field(default=None, alias="typeCode")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class OrderProduct(BrokerModel):
|
|
96
|
+
symbol: str | None = None
|
|
97
|
+
security_type: str | None = Field(default=None, alias="securityType")
|
|
98
|
+
security_sub_type: str | None = Field(default=None, alias="securitySubType")
|
|
99
|
+
call_put: str | None = Field(default=None, alias="callPut")
|
|
100
|
+
expiry_year: int | None = Field(default=None, alias="expiryYear")
|
|
101
|
+
expiry_month: int | None = Field(default=None, alias="expiryMonth")
|
|
102
|
+
expiry_day: int | None = Field(default=None, alias="expiryDay")
|
|
103
|
+
strike_price: Decimal | None = Field(default=None, alias="strikePrice")
|
|
104
|
+
expiry_type: str | None = Field(default=None, alias="expiryType")
|
|
105
|
+
|
|
106
|
+
@field_validator("symbol", "security_type", "security_sub_type", "call_put", "expiry_type")
|
|
107
|
+
@classmethod
|
|
108
|
+
def nonempty_strings(cls, value: str | None) -> str | None:
|
|
109
|
+
if value is not None and not value.strip():
|
|
110
|
+
raise ValueError("A nonempty value is required")
|
|
111
|
+
return value
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class OrderResponseProduct(OrderProduct):
|
|
115
|
+
"""Product details returned by the broker, retaining request-model compatibility."""
|
|
116
|
+
|
|
117
|
+
product_id: OrderProductId | None = Field(default=None, alias="productId")
|
|
118
|
+
|
|
119
|
+
@model_validator(mode="before")
|
|
120
|
+
@classmethod
|
|
121
|
+
def normalize_product_id(cls, value: object) -> object:
|
|
122
|
+
return _normalize_aliases(value, {"ProductId": "productId"})
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class OrderLot(BrokerModel):
|
|
126
|
+
id: int | None = None
|
|
127
|
+
size: Decimal | None = None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _empty_order_lots() -> list[OrderLot]:
|
|
131
|
+
return []
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class OrderLots(BrokerModel):
|
|
135
|
+
lots: list[OrderLot] = Field(default_factory=_empty_order_lots, alias="lot")
|
|
136
|
+
|
|
137
|
+
@model_validator(mode="before")
|
|
138
|
+
@classmethod
|
|
139
|
+
def normalize_response(cls, value: object) -> object:
|
|
140
|
+
if not isinstance(value, dict):
|
|
141
|
+
return value
|
|
142
|
+
data = dict(cast(dict[str, Any], value))
|
|
143
|
+
for source in ("Lot", "lots"):
|
|
144
|
+
if source in data and "lot" not in data:
|
|
145
|
+
data["lot"] = data[source]
|
|
146
|
+
data.pop(source, None)
|
|
147
|
+
return data
|
|
148
|
+
|
|
149
|
+
@field_validator("lots", mode="before")
|
|
150
|
+
@classmethod
|
|
151
|
+
def normalize_lots(cls, value: object) -> object:
|
|
152
|
+
if value is None:
|
|
153
|
+
return []
|
|
154
|
+
if isinstance(value, dict):
|
|
155
|
+
return [cast(dict[str, Any], value)]
|
|
156
|
+
return value
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class MFQuantity(BrokerModel):
|
|
160
|
+
cash: Decimal | None = None
|
|
161
|
+
margin: Decimal | None = None
|
|
162
|
+
cusip: str | None = None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class OrderInstrumentRequest(BaseModel):
|
|
166
|
+
"""Instrument request entry inside an E*TRADE order body."""
|
|
167
|
+
|
|
168
|
+
model_config = ConfigDict(extra="allow", frozen=True, populate_by_name=True)
|
|
169
|
+
|
|
170
|
+
product: OrderProduct = Field(alias="Product")
|
|
171
|
+
order_action: str = Field(alias="orderAction")
|
|
172
|
+
quantity_type: str | None = Field(default="QUANTITY", alias="quantityType")
|
|
173
|
+
quantity: Decimal | None = None
|
|
174
|
+
ordered_quantity: Decimal | None = Field(default=None, alias="orderedQuantity")
|
|
175
|
+
lots: OrderLots | None = None
|
|
176
|
+
mf_quantity: MFQuantity | None = Field(default=None, alias="mfQuantity")
|
|
177
|
+
osi_key: str | None = Field(default=None, alias="osiKey")
|
|
178
|
+
mf_transaction: str | None = Field(default=None, alias="mfTransaction")
|
|
179
|
+
reserve_order: bool | None = Field(default=None, alias="reserveOrder")
|
|
180
|
+
reserve_quantity: Decimal | None = Field(default=None, alias="reserveQuantity")
|
|
181
|
+
|
|
182
|
+
@field_validator("order_action", "quantity_type", "osi_key", "mf_transaction")
|
|
183
|
+
@classmethod
|
|
184
|
+
def normalize_optional_strings(cls, value: str | None) -> str | None:
|
|
185
|
+
if value is None:
|
|
186
|
+
return None
|
|
187
|
+
normalized = value.strip().upper()
|
|
188
|
+
if not normalized:
|
|
189
|
+
raise ValueError("A nonempty value is required")
|
|
190
|
+
return normalized
|
|
191
|
+
|
|
192
|
+
@field_validator("quantity", "ordered_quantity", "reserve_quantity")
|
|
193
|
+
@classmethod
|
|
194
|
+
def positive_quantities(cls, value: Decimal | None) -> Decimal | None:
|
|
195
|
+
if value is not None and (not value.is_finite() or value <= 0):
|
|
196
|
+
raise ValueError("Quantity values must be finite and positive")
|
|
197
|
+
return value
|
|
198
|
+
|
|
199
|
+
@field_validator("order_action", "quantity_type")
|
|
200
|
+
@classmethod
|
|
201
|
+
def validate_supported_values(cls, value: str | None, info: ValidationInfo) -> str | None:
|
|
202
|
+
if value is None:
|
|
203
|
+
return None
|
|
204
|
+
allowed = VALID_ORDER_ACTIONS if info.field_name == "order_action" else VALID_QUANTITY_TYPES
|
|
205
|
+
return _supported_value(value, allowed, info.field_name or "value")
|
|
206
|
+
|
|
207
|
+
@field_validator("product")
|
|
208
|
+
@classmethod
|
|
209
|
+
def validate_product(cls, value: OrderProduct) -> OrderProduct:
|
|
210
|
+
if value.symbol is None or not value.symbol.strip():
|
|
211
|
+
raise ValueError("symbol is required")
|
|
212
|
+
if value.security_type is None:
|
|
213
|
+
raise ValueError("security_type is required")
|
|
214
|
+
security_type = _supported_value(value.security_type, VALID_SECURITY_TYPES, "security_type")
|
|
215
|
+
updates: dict[str, Any] = {
|
|
216
|
+
"symbol": value.symbol.strip().upper(),
|
|
217
|
+
"security_type": security_type,
|
|
218
|
+
}
|
|
219
|
+
if security_type == "OPTN":
|
|
220
|
+
if value.call_put is None:
|
|
221
|
+
raise ValueError("call_put is required for option instruments")
|
|
222
|
+
updates["call_put"] = _supported_value(
|
|
223
|
+
value.call_put, frozenset({"CALL", "PUT"}), "call_put"
|
|
224
|
+
)
|
|
225
|
+
if value.expiry_year is None or value.expiry_month is None or value.expiry_day is None:
|
|
226
|
+
raise ValueError("expiration is required for option instruments")
|
|
227
|
+
try:
|
|
228
|
+
date(value.expiry_year, value.expiry_month, value.expiry_day)
|
|
229
|
+
except ValueError:
|
|
230
|
+
raise ValueError("expiration must be a valid calendar date") from None
|
|
231
|
+
if (
|
|
232
|
+
value.strike_price is None
|
|
233
|
+
or not value.strike_price.is_finite()
|
|
234
|
+
or value.strike_price <= 0
|
|
235
|
+
):
|
|
236
|
+
raise ValueError("strike_price must be finite and positive for option instruments")
|
|
237
|
+
return value.model_copy(update=updates)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class OrderInstrument(BrokerModel):
|
|
241
|
+
product: OrderResponseProduct | None = Field(default=None, alias="Product")
|
|
242
|
+
symbol_description: str | None = Field(default=None, alias="symbolDescription")
|
|
243
|
+
order_action: str | None = Field(default=None, alias="orderAction")
|
|
244
|
+
quantity_type: str | None = Field(default=None, alias="quantityType")
|
|
245
|
+
quantity: Decimal | None = None
|
|
246
|
+
cancel_quantity: Decimal | None = Field(default=None, alias="cancelQuantity")
|
|
247
|
+
ordered_quantity: Decimal | None = Field(default=None, alias="orderedQuantity")
|
|
248
|
+
filled_quantity: Decimal | None = Field(default=None, alias="filledQuantity")
|
|
249
|
+
average_execution_price: Decimal | None = Field(default=None, alias="averageExecutionPrice")
|
|
250
|
+
estimated_commission: Decimal | None = Field(default=None, alias="estimatedCommission")
|
|
251
|
+
estimated_fees: Decimal | None = Field(default=None, alias="estimatedFees")
|
|
252
|
+
bid: Decimal | None = None
|
|
253
|
+
ask: Decimal | None = None
|
|
254
|
+
last_price: Decimal | None = Field(default=None, alias="lastprice")
|
|
255
|
+
currency: str | None = None
|
|
256
|
+
lots: OrderLots | None = None
|
|
257
|
+
mf_quantity: MFQuantity | None = Field(default=None, alias="mfQuantity")
|
|
258
|
+
osi_key: str | None = Field(default=None, alias="osiKey")
|
|
259
|
+
reserve_order: bool | None = Field(default=None, alias="reserveOrder")
|
|
260
|
+
reserve_quantity: Decimal | None = Field(default=None, alias="reserveQuantity")
|
|
261
|
+
mf_transaction: str | None = Field(default=None, alias="mfTransaction")
|
|
262
|
+
|
|
263
|
+
@field_validator("product", mode="before")
|
|
264
|
+
@classmethod
|
|
265
|
+
def normalize_product_model(cls, value: object) -> object:
|
|
266
|
+
if isinstance(value, OrderProduct) and not isinstance(value, OrderResponseProduct):
|
|
267
|
+
return value.model_dump(by_alias=True)
|
|
268
|
+
return value
|
|
269
|
+
|
|
270
|
+
@model_validator(mode="before")
|
|
271
|
+
@classmethod
|
|
272
|
+
def normalize_response(cls, value: object) -> object:
|
|
273
|
+
if not isinstance(value, dict):
|
|
274
|
+
return value
|
|
275
|
+
data = dict(cast(dict[str, Any], value))
|
|
276
|
+
aliases = {"product": "Product", "Lots": "lots", "MFQuantity": "mfQuantity"}
|
|
277
|
+
for source, target in aliases.items():
|
|
278
|
+
if source in data and target not in data:
|
|
279
|
+
data[target] = data[source]
|
|
280
|
+
data.pop(source, None)
|
|
281
|
+
return data
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class Message(BrokerModel):
|
|
285
|
+
description: str | None = None
|
|
286
|
+
code: int | None = None
|
|
287
|
+
type: str | None = None
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _empty_messages() -> list[Message]:
|
|
291
|
+
return []
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
class Messages(BrokerModel):
|
|
295
|
+
messages: list[Message] = Field(default_factory=_empty_messages, alias="message")
|
|
296
|
+
|
|
297
|
+
@model_validator(mode="before")
|
|
298
|
+
@classmethod
|
|
299
|
+
def normalize_response(cls, value: object) -> object:
|
|
300
|
+
if not isinstance(value, dict):
|
|
301
|
+
return value
|
|
302
|
+
data = dict(cast(dict[str, Any], value))
|
|
303
|
+
for source in ("Message", "messages"):
|
|
304
|
+
if source in data and "message" not in data:
|
|
305
|
+
data["message"] = data[source]
|
|
306
|
+
data.pop(source, None)
|
|
307
|
+
return data
|
|
308
|
+
|
|
309
|
+
@field_validator("messages", mode="before")
|
|
310
|
+
@classmethod
|
|
311
|
+
def normalize_messages(cls, value: object) -> object:
|
|
312
|
+
if value is None:
|
|
313
|
+
return []
|
|
314
|
+
if isinstance(value, dict):
|
|
315
|
+
return [cast(dict[str, Any], value)]
|
|
316
|
+
return value
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class OrderDetailRequest(BaseModel):
|
|
320
|
+
"""Order detail request entry inside preview/place requests."""
|
|
321
|
+
|
|
322
|
+
model_config = ConfigDict(extra="allow", frozen=True, populate_by_name=True)
|
|
323
|
+
|
|
324
|
+
all_or_none: bool | str | None = Field(default=None, alias="allOrNone")
|
|
325
|
+
price_type: str = Field(alias="priceType")
|
|
326
|
+
order_term: str = Field(alias="orderTerm")
|
|
327
|
+
market_session: str | None = Field(default="REGULAR", alias="marketSession")
|
|
328
|
+
stop_price: Decimal | str | None = Field(default=None, alias="stopPrice")
|
|
329
|
+
limit_price: Decimal | None = Field(default=None, alias="limitPrice")
|
|
330
|
+
stop_limit_price: Decimal | None = Field(default=None, alias="stopLimitPrice")
|
|
331
|
+
offset_type: str | None = Field(default=None, alias="offsetType")
|
|
332
|
+
offset_value: Decimal | None = Field(default=None, alias="offsetValue")
|
|
333
|
+
routing_destination: str | None = Field(default=None, alias="routingDestination")
|
|
334
|
+
disclosure: Disclosure | None = Field(default=None, alias="disclosure")
|
|
335
|
+
instruments: list[OrderInstrumentRequest] = Field(alias="Instrument")
|
|
336
|
+
|
|
337
|
+
@field_validator(
|
|
338
|
+
"price_type", "order_term", "market_session", "offset_type", "routing_destination"
|
|
339
|
+
)
|
|
340
|
+
@classmethod
|
|
341
|
+
def normalize_optional_strings(cls, value: str | None) -> str | None:
|
|
342
|
+
if value is None:
|
|
343
|
+
return None
|
|
344
|
+
normalized = value.strip().upper()
|
|
345
|
+
if not normalized:
|
|
346
|
+
raise ValueError("A nonempty value is required")
|
|
347
|
+
return normalized
|
|
348
|
+
|
|
349
|
+
@field_validator("price_type", "order_term", "market_session")
|
|
350
|
+
@classmethod
|
|
351
|
+
def validate_supported_values(cls, value: str | None, info: ValidationInfo) -> str | None:
|
|
352
|
+
if value is None:
|
|
353
|
+
return None
|
|
354
|
+
allowed = {
|
|
355
|
+
"price_type": VALID_PRICE_TYPES,
|
|
356
|
+
"order_term": VALID_ORDER_TERMS,
|
|
357
|
+
"market_session": VALID_MARKET_SESSIONS,
|
|
358
|
+
}
|
|
359
|
+
return _supported_value(value, allowed[info.field_name or ""], info.field_name or "value")
|
|
360
|
+
|
|
361
|
+
@field_validator("stop_price", mode="before")
|
|
362
|
+
@classmethod
|
|
363
|
+
def parse_stop_price(cls, value: object) -> object:
|
|
364
|
+
if value is None or value == "":
|
|
365
|
+
return value
|
|
366
|
+
try:
|
|
367
|
+
return Decimal(str(value))
|
|
368
|
+
except Exception:
|
|
369
|
+
raise ValueError("stop_price must be a decimal value") from None
|
|
370
|
+
|
|
371
|
+
@field_validator("limit_price", "stop_limit_price", "stop_price")
|
|
372
|
+
@classmethod
|
|
373
|
+
def finite_prices(cls, value: Decimal | str | None) -> Decimal | str | None:
|
|
374
|
+
if isinstance(value, Decimal) and not value.is_finite():
|
|
375
|
+
raise ValueError("Prices must be finite")
|
|
376
|
+
return value
|
|
377
|
+
|
|
378
|
+
@field_validator("instruments", mode="before")
|
|
379
|
+
@classmethod
|
|
380
|
+
def normalize_instruments(cls, value: object) -> object:
|
|
381
|
+
if isinstance(value, dict):
|
|
382
|
+
return [cast(dict[str, Any], value)]
|
|
383
|
+
return value
|
|
384
|
+
|
|
385
|
+
@model_validator(mode="after")
|
|
386
|
+
def validate_price_shape(self) -> OrderDetailRequest:
|
|
387
|
+
if not self.instruments:
|
|
388
|
+
raise ValueError("At least one instrument is required")
|
|
389
|
+
if self.price_type in {
|
|
390
|
+
"LIMIT",
|
|
391
|
+
"NET_DEBIT",
|
|
392
|
+
"NET_CREDIT",
|
|
393
|
+
"LIMIT_ON_OPEN",
|
|
394
|
+
"LIMIT_ON_CLOSE",
|
|
395
|
+
}:
|
|
396
|
+
if self.limit_price is None:
|
|
397
|
+
raise ValueError("limit_price is required for limit price types")
|
|
398
|
+
if self.limit_price <= 0:
|
|
399
|
+
raise ValueError("limit_price must be positive")
|
|
400
|
+
if self.price_type in {"STOP", "STOP_LIMIT"} and self.stop_price in (None, ""):
|
|
401
|
+
raise ValueError("stop_price is required for stop price types")
|
|
402
|
+
if self.price_type in {"STOP", "STOP_LIMIT"}:
|
|
403
|
+
if not isinstance(self.stop_price, Decimal) or self.stop_price <= 0:
|
|
404
|
+
raise ValueError("stop_price must be positive")
|
|
405
|
+
if self.price_type == "STOP_LIMIT":
|
|
406
|
+
if self.limit_price is None and self.stop_limit_price is None:
|
|
407
|
+
raise ValueError("limit_price or stop_limit_price is required for STOP_LIMIT")
|
|
408
|
+
if any(
|
|
409
|
+
price is not None and price <= 0
|
|
410
|
+
for price in (self.limit_price, self.stop_limit_price)
|
|
411
|
+
):
|
|
412
|
+
raise ValueError("STOP_LIMIT limit prices must be positive")
|
|
413
|
+
if self.price_type == "MARKET" and self.limit_price is not None:
|
|
414
|
+
raise ValueError("limit_price is not valid for MARKET orders")
|
|
415
|
+
return self
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _empty_order_instruments() -> list[OrderInstrument]:
|
|
419
|
+
return []
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
class OrderDetail(BrokerModel):
|
|
423
|
+
order_number: int | None = Field(default=None, alias="orderNumber")
|
|
424
|
+
account_id: str | None = Field(default=None, alias="accountId")
|
|
425
|
+
preview_time: datetime | None = Field(default=None, alias="previewTime")
|
|
426
|
+
placed_time: datetime | None = Field(default=None, alias="placedTime")
|
|
427
|
+
executed_time: datetime | None = Field(default=None, alias="executedTime")
|
|
428
|
+
order_value: Decimal | None = Field(default=None, alias="orderValue")
|
|
429
|
+
status: str | None = None
|
|
430
|
+
order_type: str | None = Field(default=None, alias="orderType")
|
|
431
|
+
order_term: str | None = Field(default=None, alias="orderTerm")
|
|
432
|
+
price_type: str | None = Field(default=None, alias="priceType")
|
|
433
|
+
price_value: str | None = Field(default=None, alias="priceValue")
|
|
434
|
+
limit_price: Decimal | None = Field(default=None, alias="limitPrice")
|
|
435
|
+
stop_price: Decimal | None = Field(default=None, alias="stopPrice")
|
|
436
|
+
stop_limit_price: Decimal | None = Field(default=None, alias="stopLimitPrice")
|
|
437
|
+
offset_type: str | None = Field(default=None, alias="offsetType")
|
|
438
|
+
offset_value: Decimal | None = Field(default=None, alias="offsetValue")
|
|
439
|
+
market_session: str | None = Field(default=None, alias="marketSession")
|
|
440
|
+
all_or_none: bool | str | None = Field(default=None, alias="allOrNone")
|
|
441
|
+
messages: Messages | None = None
|
|
442
|
+
estimated_commission: Decimal | None = Field(default=None, alias="estimatedCommission")
|
|
443
|
+
estimated_total_amount: Decimal | None = Field(default=None, alias="estimatedTotalAmount")
|
|
444
|
+
net_price: Decimal | None = Field(default=None, alias="netPrice")
|
|
445
|
+
net_bid: Decimal | None = Field(default=None, alias="netBid")
|
|
446
|
+
net_ask: Decimal | None = Field(default=None, alias="netAsk")
|
|
447
|
+
ratio: str | None = None
|
|
448
|
+
eg_qual: str | None = Field(default=None, alias="egQual")
|
|
449
|
+
instruments: list[OrderInstrument] = Field(
|
|
450
|
+
default_factory=_empty_order_instruments, alias="Instrument"
|
|
451
|
+
)
|
|
452
|
+
routing_destination: str | None = Field(default=None, alias="routingDestination")
|
|
453
|
+
bracketed_limit_price: Decimal | None = Field(default=None, alias="bracketedLimitPrice")
|
|
454
|
+
initial_stop_price: Decimal | None = Field(default=None, alias="initialStopPrice")
|
|
455
|
+
trail_price: Decimal | None = Field(default=None, alias="trailPrice")
|
|
456
|
+
trigger_price: Decimal | None = Field(default=None, alias="triggerPrice")
|
|
457
|
+
condition_price: Decimal | None = Field(default=None, alias="conditionPrice")
|
|
458
|
+
condition_symbol: str | None = Field(default=None, alias="conditionSymbol")
|
|
459
|
+
condition_type: str | None = Field(default=None, alias="conditionType")
|
|
460
|
+
condition_follow_price: str | None = Field(default=None, alias="conditionFollowPrice")
|
|
461
|
+
condition_security_type: str | None = Field(default=None, alias="conditionSecurityType")
|
|
462
|
+
replaced_by_order_id: int | None = Field(default=None, alias="replacedByOrderId")
|
|
463
|
+
replaces_order_id: int | None = Field(default=None, alias="replacesOrderId")
|
|
464
|
+
preview_id: int | None = Field(default=None, alias="previewId")
|
|
465
|
+
investment_amount: Decimal | None = Field(default=None, alias="investmentAmount")
|
|
466
|
+
position_quantity: str | None = Field(default=None, alias="positionQuantity")
|
|
467
|
+
aip_flag: bool | None = Field(default=None, alias="aipFlag")
|
|
468
|
+
re_invest_option: str | None = Field(default=None, alias="reInvestOption")
|
|
469
|
+
estimated_fees: Decimal | None = Field(default=None, alias="estimatedFees")
|
|
470
|
+
gcd: int | None = None
|
|
471
|
+
mf_price_type: str | None = Field(default=None, alias="mfpriceType")
|
|
472
|
+
|
|
473
|
+
@field_validator("preview_time", "placed_time", "executed_time", mode="before")
|
|
474
|
+
@classmethod
|
|
475
|
+
def parse_times(cls, value: object) -> datetime | None:
|
|
476
|
+
return parse_broker_datetime(value)
|
|
477
|
+
|
|
478
|
+
@model_validator(mode="before")
|
|
479
|
+
@classmethod
|
|
480
|
+
def normalize_response(cls, value: object) -> object:
|
|
481
|
+
if not isinstance(value, dict):
|
|
482
|
+
return value
|
|
483
|
+
data = dict(cast(dict[str, Any], value))
|
|
484
|
+
aliases = {"instrument": "Instrument", "messages": "messages", "Messages": "messages"}
|
|
485
|
+
for source, target in aliases.items():
|
|
486
|
+
if source in data and target not in data:
|
|
487
|
+
data[target] = data[source]
|
|
488
|
+
if source != target:
|
|
489
|
+
data.pop(source, None)
|
|
490
|
+
if isinstance(data.get("Instrument"), dict):
|
|
491
|
+
nested = cast(dict[str, Any], data["Instrument"])
|
|
492
|
+
data["Instrument"] = nested.get("Instrument", nested)
|
|
493
|
+
if isinstance(data["Instrument"], dict):
|
|
494
|
+
data["Instrument"] = [data["Instrument"]]
|
|
495
|
+
return data
|
|
496
|
+
|
|
497
|
+
@field_validator("instruments", mode="before")
|
|
498
|
+
@classmethod
|
|
499
|
+
def normalize_instruments(cls, value: object) -> object:
|
|
500
|
+
if value is None:
|
|
501
|
+
return []
|
|
502
|
+
if isinstance(value, dict):
|
|
503
|
+
nested = cast(dict[str, Any], value)
|
|
504
|
+
if "Instrument" in nested:
|
|
505
|
+
return nested["Instrument"]
|
|
506
|
+
return [nested]
|
|
507
|
+
return value
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _empty_event_instruments() -> list[OrderInstrument]:
|
|
511
|
+
return []
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
class OrderEvent(BrokerModel):
|
|
515
|
+
name: str | None = None
|
|
516
|
+
date_time: datetime | None = Field(default=None, alias="dateTime")
|
|
517
|
+
order_number: int | None = Field(default=None, alias="orderNumber")
|
|
518
|
+
instruments: list[OrderInstrument] = Field(
|
|
519
|
+
default_factory=_empty_event_instruments, alias="instrument"
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
@model_validator(mode="before")
|
|
523
|
+
@classmethod
|
|
524
|
+
def normalize_response(cls, value: object) -> object:
|
|
525
|
+
if not isinstance(value, dict):
|
|
526
|
+
return value
|
|
527
|
+
data = dict(cast(dict[str, Any], value))
|
|
528
|
+
if isinstance(data.get("instrument"), dict):
|
|
529
|
+
nested = cast(dict[str, Any], data["instrument"])
|
|
530
|
+
data["instrument"] = nested.get("Instrument", nested)
|
|
531
|
+
if isinstance(data["instrument"], dict):
|
|
532
|
+
data["instrument"] = [data["instrument"]]
|
|
533
|
+
return data
|
|
534
|
+
|
|
535
|
+
@field_validator("date_time", mode="before")
|
|
536
|
+
@classmethod
|
|
537
|
+
def parse_date_time(cls, value: object) -> datetime | None:
|
|
538
|
+
return parse_broker_datetime(value)
|
|
539
|
+
|
|
540
|
+
@field_validator("instruments", mode="before")
|
|
541
|
+
@classmethod
|
|
542
|
+
def normalize_instruments(cls, value: object) -> object:
|
|
543
|
+
if value is None:
|
|
544
|
+
return []
|
|
545
|
+
if isinstance(value, dict):
|
|
546
|
+
nested = cast(dict[str, Any], value)
|
|
547
|
+
if "Instrument" in nested:
|
|
548
|
+
return nested["Instrument"]
|
|
549
|
+
return [nested]
|
|
550
|
+
return value
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _empty_order_events() -> list[OrderEvent]:
|
|
554
|
+
return []
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
class Events(BrokerModel):
|
|
558
|
+
events: list[OrderEvent] = Field(default_factory=_empty_order_events, alias="event")
|
|
559
|
+
|
|
560
|
+
@field_validator("events", mode="before")
|
|
561
|
+
@classmethod
|
|
562
|
+
def normalize_events(cls, value: object) -> object:
|
|
563
|
+
if value is None:
|
|
564
|
+
return []
|
|
565
|
+
if isinstance(value, dict):
|
|
566
|
+
return [cast(dict[str, Any], value)]
|
|
567
|
+
return value
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def _empty_order_details() -> list[OrderDetail]:
|
|
571
|
+
return []
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
class Order(BrokerModel):
|
|
575
|
+
order_id: int | None = Field(default=None, alias="orderId")
|
|
576
|
+
details: str | None = None
|
|
577
|
+
order_type: str | None = Field(default=None, alias="orderType")
|
|
578
|
+
total_order_value: Decimal | None = Field(default=None, alias="totalOrderValue")
|
|
579
|
+
total_commission: Decimal | None = Field(default=None, alias="totalCommission")
|
|
580
|
+
order_details: list[OrderDetail] = Field(
|
|
581
|
+
default_factory=_empty_order_details, alias="orderDetail"
|
|
582
|
+
)
|
|
583
|
+
events: Events | None = None
|
|
584
|
+
|
|
585
|
+
@model_validator(mode="before")
|
|
586
|
+
@classmethod
|
|
587
|
+
def normalize_response(cls, value: object) -> object:
|
|
588
|
+
if not isinstance(value, dict):
|
|
589
|
+
return value
|
|
590
|
+
data = dict(cast(dict[str, Any], value))
|
|
591
|
+
for source in ("OrderDetail", "orderDetails"):
|
|
592
|
+
if source in data and "orderDetail" not in data:
|
|
593
|
+
data["orderDetail"] = data[source]
|
|
594
|
+
data.pop(source, None)
|
|
595
|
+
if "events" in data and isinstance(data["events"], dict) and "event" not in data["events"]:
|
|
596
|
+
data["events"] = {"event": data["events"]}
|
|
597
|
+
return data
|
|
598
|
+
|
|
599
|
+
@field_validator("order_details", mode="before")
|
|
600
|
+
@classmethod
|
|
601
|
+
def normalize_details(cls, value: object) -> object:
|
|
602
|
+
if value is None:
|
|
603
|
+
return []
|
|
604
|
+
if isinstance(value, dict):
|
|
605
|
+
return [cast(dict[str, Any], value)]
|
|
606
|
+
return value
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
class PreviewId(BaseModel):
|
|
610
|
+
model_config = ConfigDict(frozen=True, populate_by_name=True)
|
|
611
|
+
|
|
612
|
+
preview_id: int = Field(alias="previewId", strict=True, gt=0)
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
class OrderId(BaseModel):
|
|
616
|
+
model_config = ConfigDict(frozen=True, populate_by_name=True)
|
|
617
|
+
|
|
618
|
+
order_id: int = Field(alias="orderId", strict=True, gt=0)
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
class PreviewResponseId(PreviewId, BrokerModel):
|
|
622
|
+
"""Preview ID and broker margin designation; placement requests use PreviewId."""
|
|
623
|
+
|
|
624
|
+
preview_id: int = Field(alias="previewId")
|
|
625
|
+
cash_margin: str | None = Field(default=None, alias="cashMargin")
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
class OrderResponseId(OrderId, BrokerModel):
|
|
629
|
+
"""Order ID and margin designation returned by the broker."""
|
|
630
|
+
|
|
631
|
+
order_id: int = Field(alias="orderId")
|
|
632
|
+
cash_margin: str | None = Field(default=None, alias="cashMargin")
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
class OrderBuyPowerEffect(BrokerModel):
|
|
636
|
+
current_bp: Decimal | None = Field(default=None, alias="currentBp")
|
|
637
|
+
current_oor: Decimal | None = Field(default=None, alias="currentOor")
|
|
638
|
+
current_net_bp: Decimal | None = Field(default=None, alias="currentNetBp")
|
|
639
|
+
current_order_impact: Decimal | None = Field(default=None, alias="currentOrderImpact")
|
|
640
|
+
net_bp: Decimal | None = Field(default=None, alias="netBp")
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
class CashBuyingPowerDetails(BrokerModel):
|
|
644
|
+
settled: OrderBuyPowerEffect | None = None
|
|
645
|
+
settled_unsettled: OrderBuyPowerEffect | None = Field(default=None, alias="settledUnsettled")
|
|
646
|
+
|
|
647
|
+
@model_validator(mode="before")
|
|
648
|
+
@classmethod
|
|
649
|
+
def normalize_response(cls, value: object) -> object:
|
|
650
|
+
return _normalize_aliases(
|
|
651
|
+
value, {"Settled": "settled", "SettledUnsettled": "settledUnsettled"}
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
class MarginBuyingPowerDetails(BrokerModel):
|
|
656
|
+
non_marginable: OrderBuyPowerEffect | None = Field(default=None, alias="nonMarginable")
|
|
657
|
+
marginable: OrderBuyPowerEffect | None = None
|
|
658
|
+
|
|
659
|
+
@model_validator(mode="before")
|
|
660
|
+
@classmethod
|
|
661
|
+
def normalize_response(cls, value: object) -> object:
|
|
662
|
+
return _normalize_aliases(
|
|
663
|
+
value, {"NonMarginable": "nonMarginable", "Marginable": "marginable"}
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
class DtBuyingPowerDetails(MarginBuyingPowerDetails):
|
|
668
|
+
"""Day-trading buying-power effects for marginable and non-marginable securities."""
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
class PortfolioMargin(BrokerModel):
|
|
672
|
+
house_excess_equity_new: Decimal | None = Field(default=None, alias="houseExcessEquityNew")
|
|
673
|
+
pm_eligible: bool | None = Field(default=None, alias="pmEligible")
|
|
674
|
+
house_excess_equity_curr: Decimal | None = Field(default=None, alias="houseExcessEquityCurr")
|
|
675
|
+
house_excess_equity_change: Decimal | None = Field(
|
|
676
|
+
default=None, alias="houseExcessEquityChange"
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
class Disclosure(BrokerModel):
|
|
681
|
+
eh_disclosure_flag: bool | None = Field(default=None, alias="ehDisclosureFlag")
|
|
682
|
+
ah_disclosure_flag: bool | None = Field(default=None, alias="ahDisclosureFlag")
|
|
683
|
+
conditional_disclosure_flag: bool | None = Field(
|
|
684
|
+
default=None, alias="conditionalDisclosureFlag"
|
|
685
|
+
)
|
|
686
|
+
ao_disclosure_flag: bool | None = Field(default=None, alias="aoDisclosureFlag")
|
|
687
|
+
mf_fl_consent: bool | None = Field(default=None, alias="mfFLConsent")
|
|
688
|
+
mf_eo_consent: bool | None = Field(default=None, alias="mfEOConsent")
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
class OrdersRequest(BaseModel):
|
|
692
|
+
"""Query parameters for listing orders."""
|
|
693
|
+
|
|
694
|
+
model_config = ConfigDict(frozen=True, populate_by_name=True)
|
|
695
|
+
|
|
696
|
+
marker: str | None = None
|
|
697
|
+
count: int | None = Field(default=None, ge=1, le=100)
|
|
698
|
+
status: str | None = None
|
|
699
|
+
from_date: str | None = Field(default=None, alias="fromDate")
|
|
700
|
+
to_date: str | None = Field(default=None, alias="toDate")
|
|
701
|
+
symbol: str | None = None
|
|
702
|
+
security_type: str | None = Field(default=None, alias="securityType")
|
|
703
|
+
transaction_type: str | None = Field(default=None, alias="transactionType")
|
|
704
|
+
market_session: str | None = Field(default=None, alias="marketSession")
|
|
705
|
+
|
|
706
|
+
@field_validator(
|
|
707
|
+
"marker",
|
|
708
|
+
"status",
|
|
709
|
+
"from_date",
|
|
710
|
+
"to_date",
|
|
711
|
+
"symbol",
|
|
712
|
+
"security_type",
|
|
713
|
+
"transaction_type",
|
|
714
|
+
"market_session",
|
|
715
|
+
)
|
|
716
|
+
@classmethod
|
|
717
|
+
def normalize_strings(cls, value: str | None) -> str | None:
|
|
718
|
+
if value is None:
|
|
719
|
+
return None
|
|
720
|
+
normalized = value.strip()
|
|
721
|
+
if not normalized:
|
|
722
|
+
raise ValueError("A nonempty value is required")
|
|
723
|
+
return normalized
|
|
724
|
+
|
|
725
|
+
def query_params(self) -> dict[str, str | int | None]:
|
|
726
|
+
return {
|
|
727
|
+
"marker": self.marker,
|
|
728
|
+
"count": self.count,
|
|
729
|
+
"status": self.status,
|
|
730
|
+
"fromDate": self.from_date,
|
|
731
|
+
"toDate": self.to_date,
|
|
732
|
+
"symbol": self.symbol,
|
|
733
|
+
"securityType": self.security_type,
|
|
734
|
+
"transactionType": self.transaction_type,
|
|
735
|
+
"marketSession": self.market_session,
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
class PreviewOrderRequest(BaseModel):
|
|
740
|
+
"""Request body for previewing a new or changed order."""
|
|
741
|
+
|
|
742
|
+
model_config = ConfigDict(frozen=True, populate_by_name=True)
|
|
743
|
+
|
|
744
|
+
order_type: str = Field(alias="orderType")
|
|
745
|
+
client_order_id: str = Field(alias="clientOrderId")
|
|
746
|
+
orders: list[OrderDetailRequest] = Field(alias="Order")
|
|
747
|
+
|
|
748
|
+
@field_validator("order_type")
|
|
749
|
+
@classmethod
|
|
750
|
+
def normalize_order_type(cls, value: str) -> str:
|
|
751
|
+
return _supported_value(value, VALID_ORDER_TYPES, "order_type")
|
|
752
|
+
|
|
753
|
+
@field_validator("client_order_id")
|
|
754
|
+
@classmethod
|
|
755
|
+
def valid_client_order_id(cls, value: str) -> str:
|
|
756
|
+
normalized = value.strip()
|
|
757
|
+
if not _CLIENT_ORDER_ID_RE.fullmatch(normalized):
|
|
758
|
+
raise ValueError("client_order_id must be 1-20 alphanumeric characters")
|
|
759
|
+
return normalized
|
|
760
|
+
|
|
761
|
+
@field_validator("orders", mode="before")
|
|
762
|
+
@classmethod
|
|
763
|
+
def normalize_orders(cls, value: object) -> object:
|
|
764
|
+
if isinstance(value, dict):
|
|
765
|
+
return [cast(dict[str, Any], value)]
|
|
766
|
+
return value
|
|
767
|
+
|
|
768
|
+
@model_validator(mode="after")
|
|
769
|
+
def validate_orders(self) -> PreviewOrderRequest:
|
|
770
|
+
if not self.orders:
|
|
771
|
+
raise ValueError("At least one order is required")
|
|
772
|
+
return self
|
|
773
|
+
|
|
774
|
+
def request_body(self) -> dict[str, Any]:
|
|
775
|
+
payload = self.model_dump(by_alias=True, exclude_none=True, mode="json")
|
|
776
|
+
return {"PreviewOrderRequest": cast(dict[str, Any], _strip_broker_metadata(payload))}
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
class PlaceOrderRequest(PreviewOrderRequest):
|
|
780
|
+
"""Request body for placing a previewed new or changed order."""
|
|
781
|
+
|
|
782
|
+
preview_ids: list[PreviewId] = Field(alias="PreviewIds")
|
|
783
|
+
|
|
784
|
+
@field_validator("preview_ids", mode="before")
|
|
785
|
+
@classmethod
|
|
786
|
+
def normalize_preview_ids(cls, value: object) -> object:
|
|
787
|
+
if value is None:
|
|
788
|
+
return value
|
|
789
|
+
if isinstance(value, int):
|
|
790
|
+
return [{"previewId": value}]
|
|
791
|
+
if isinstance(value, dict):
|
|
792
|
+
return [cast(dict[str, Any], value)]
|
|
793
|
+
if isinstance(value, list):
|
|
794
|
+
items = cast(list[object], value)
|
|
795
|
+
return [{"previewId": item} if isinstance(item, int) else item for item in items]
|
|
796
|
+
return value
|
|
797
|
+
|
|
798
|
+
@model_validator(mode="after")
|
|
799
|
+
def validate_preview_ids(self) -> PlaceOrderRequest:
|
|
800
|
+
if not self.preview_ids:
|
|
801
|
+
raise ValueError("At least one preview ID is required")
|
|
802
|
+
for preview in self.preview_ids:
|
|
803
|
+
PreviewId.model_validate({"previewId": preview.preview_id})
|
|
804
|
+
return self
|
|
805
|
+
|
|
806
|
+
def request_body(self) -> dict[str, Any]:
|
|
807
|
+
payload = self.model_dump(by_alias=True, exclude_none=True, mode="json")
|
|
808
|
+
return {"PlaceOrderRequest": cast(dict[str, Any], _strip_broker_metadata(payload))}
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
class CancelOrderRequest(BaseModel):
|
|
812
|
+
model_config = ConfigDict(frozen=True, populate_by_name=True)
|
|
813
|
+
|
|
814
|
+
order_id: int = Field(alias="orderId", strict=True, ge=1)
|
|
815
|
+
|
|
816
|
+
def request_body(self) -> dict[str, Any]:
|
|
817
|
+
return {"CancelOrderRequest": self.model_dump(by_alias=True, mode="json")}
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
def _empty_orders() -> list[Order]:
|
|
821
|
+
return []
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
class OrdersResponse(BrokerModel):
|
|
825
|
+
orders: list[Order] = Field(default_factory=_empty_orders, alias="order")
|
|
826
|
+
marker: str | None = None
|
|
827
|
+
next: str | None = None
|
|
828
|
+
messages: Messages | None = None
|
|
829
|
+
|
|
830
|
+
@model_validator(mode="before")
|
|
831
|
+
@classmethod
|
|
832
|
+
def normalize_response(cls, value: object) -> object:
|
|
833
|
+
if not isinstance(value, dict):
|
|
834
|
+
return value
|
|
835
|
+
data = dict(cast(dict[str, Any], value))
|
|
836
|
+
for source in ("Order", "orders"):
|
|
837
|
+
if source in data and "order" not in data:
|
|
838
|
+
data["order"] = data[source]
|
|
839
|
+
data.pop(source, None)
|
|
840
|
+
for source in ("Messages",):
|
|
841
|
+
if source in data and "messages" not in data:
|
|
842
|
+
data["messages"] = data[source]
|
|
843
|
+
data.pop(source, None)
|
|
844
|
+
return data
|
|
845
|
+
|
|
846
|
+
@field_validator("orders", mode="before")
|
|
847
|
+
@classmethod
|
|
848
|
+
def normalize_orders(cls, value: object) -> object:
|
|
849
|
+
if value is None:
|
|
850
|
+
return []
|
|
851
|
+
if isinstance(value, dict):
|
|
852
|
+
return [cast(dict[str, Any], value)]
|
|
853
|
+
return value
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
def _empty_response_order_details() -> list[OrderDetail]:
|
|
857
|
+
return []
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
def _empty_preview_ids() -> list[PreviewResponseId]:
|
|
861
|
+
return []
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
class PreviewOrderResponse(BrokerModel):
|
|
865
|
+
order_type: str | None = Field(default=None, alias="orderType")
|
|
866
|
+
total_order_value: Decimal | None = Field(default=None, alias="totalOrderValue")
|
|
867
|
+
preview_time: datetime | None = Field(default=None, alias="previewTime")
|
|
868
|
+
dst_flag: bool | None = Field(default=None, alias="dstFlag")
|
|
869
|
+
account_id: str | None = Field(default=None, alias="accountId")
|
|
870
|
+
option_level_cd: int | None = Field(default=None, alias="optionLevelCd")
|
|
871
|
+
margin_level_cd: str | None = Field(default=None, alias="marginLevelCd")
|
|
872
|
+
orders: list[OrderDetail] = Field(default_factory=_empty_response_order_details, alias="Order")
|
|
873
|
+
preview_ids: list[PreviewResponseId] = Field(
|
|
874
|
+
default_factory=_empty_preview_ids, alias="PreviewIds"
|
|
875
|
+
)
|
|
876
|
+
disclosure: Disclosure | None = Field(default=None, alias="Disclosure")
|
|
877
|
+
settled: OrderBuyPowerEffect | None = None
|
|
878
|
+
settled_unsettled: OrderBuyPowerEffect | None = Field(default=None, alias="settledUnsettled")
|
|
879
|
+
message_list: Messages | None = Field(default=None, alias="messageList")
|
|
880
|
+
total_commission: Decimal | None = Field(default=None, alias="totalCommission")
|
|
881
|
+
portfolio_margin: PortfolioMargin | None = Field(default=None, alias="portfolioMargin")
|
|
882
|
+
is_employee: bool | None = Field(default=None, alias="isEmployee")
|
|
883
|
+
commission_message: str | None = Field(default=None, alias="commissionMessage")
|
|
884
|
+
client_order_id: str | None = Field(default=None, alias="clientOrderId")
|
|
885
|
+
margin_bp_details: MarginBuyingPowerDetails | None = Field(
|
|
886
|
+
default=None, alias="marginBpDetails"
|
|
887
|
+
)
|
|
888
|
+
cash_bp_details: CashBuyingPowerDetails | None = Field(default=None, alias="cashBpDetails")
|
|
889
|
+
dt_bp_details: DtBuyingPowerDetails | None = Field(default=None, alias="dtBpDetails")
|
|
890
|
+
|
|
891
|
+
@model_validator(mode="before")
|
|
892
|
+
@classmethod
|
|
893
|
+
def normalize_response(cls, value: object) -> object:
|
|
894
|
+
return _normalize_aliases(
|
|
895
|
+
value,
|
|
896
|
+
{
|
|
897
|
+
"order": "Order",
|
|
898
|
+
"previewIds": "PreviewIds",
|
|
899
|
+
"disclosure": "Disclosure",
|
|
900
|
+
"MessageList": "messageList",
|
|
901
|
+
"Messages": "messageList",
|
|
902
|
+
"messages": "messageList",
|
|
903
|
+
"PortfolioMargin": "portfolioMargin",
|
|
904
|
+
"MarginBpDetails": "marginBpDetails",
|
|
905
|
+
"CashBpDetails": "cashBpDetails",
|
|
906
|
+
"DtBpDetails": "dtBpDetails",
|
|
907
|
+
},
|
|
908
|
+
)
|
|
909
|
+
|
|
910
|
+
@field_validator("preview_time", mode="before")
|
|
911
|
+
@classmethod
|
|
912
|
+
def parse_preview_time(cls, value: object) -> datetime | None:
|
|
913
|
+
return parse_broker_datetime(value)
|
|
914
|
+
|
|
915
|
+
@field_validator("preview_ids", mode="before")
|
|
916
|
+
@classmethod
|
|
917
|
+
def normalize_preview_ids(cls, value: object) -> object:
|
|
918
|
+
return _normalize_response_ids(value, "previewId", "PreviewId")
|
|
919
|
+
|
|
920
|
+
@field_validator("orders", mode="before")
|
|
921
|
+
@classmethod
|
|
922
|
+
def normalize_lists(cls, value: object) -> object:
|
|
923
|
+
if value is None:
|
|
924
|
+
return []
|
|
925
|
+
if isinstance(value, dict):
|
|
926
|
+
return [cast(dict[str, Any], value)]
|
|
927
|
+
return value
|
|
928
|
+
|
|
929
|
+
|
|
930
|
+
def _empty_order_ids() -> list[OrderResponseId]:
|
|
931
|
+
return []
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
class PlaceOrderResponse(BrokerModel):
|
|
935
|
+
order_type: str | None = Field(default=None, alias="orderType")
|
|
936
|
+
placed_time: datetime | None = Field(default=None, alias="placedTime")
|
|
937
|
+
dst_flag: bool | None = Field(default=None, alias="dstFlag")
|
|
938
|
+
account_id: str | None = Field(default=None, alias="accountId")
|
|
939
|
+
option_level_cd: int | None = Field(default=None, alias="optionLevelCd")
|
|
940
|
+
margin_level_cd: str | None = Field(default=None, alias="marginLevelCd")
|
|
941
|
+
orders: list[OrderDetail] = Field(default_factory=_empty_response_order_details, alias="Order")
|
|
942
|
+
order_ids: list[OrderResponseId] = Field(default_factory=_empty_order_ids, alias="OrderIds")
|
|
943
|
+
message_list: Messages | None = Field(default=None, alias="messageList")
|
|
944
|
+
total_order_value: Decimal | None = Field(default=None, alias="totalOrderValue")
|
|
945
|
+
total_commission: Decimal | None = Field(default=None, alias="totalCommission")
|
|
946
|
+
order_id: int | None = Field(default=None, alias="orderId")
|
|
947
|
+
is_employee: bool | None = Field(default=None, alias="isEmployee")
|
|
948
|
+
commission_msg: str | None = Field(default=None, alias="commissionMsg")
|
|
949
|
+
portfolio_margin: PortfolioMargin | None = Field(default=None, alias="portfolioMargin")
|
|
950
|
+
disclosure: Disclosure | None = None
|
|
951
|
+
client_order_id: str | None = Field(default=None, alias="clientOrderId")
|
|
952
|
+
|
|
953
|
+
@model_validator(mode="before")
|
|
954
|
+
@classmethod
|
|
955
|
+
def normalize_response(cls, value: object) -> object:
|
|
956
|
+
return _normalize_aliases(
|
|
957
|
+
value,
|
|
958
|
+
{
|
|
959
|
+
"order": "Order",
|
|
960
|
+
"orderIds": "OrderIds",
|
|
961
|
+
"Disclosure": "disclosure",
|
|
962
|
+
"MessageList": "messageList",
|
|
963
|
+
"Messages": "messageList",
|
|
964
|
+
"messages": "messageList",
|
|
965
|
+
"PortfolioMargin": "portfolioMargin",
|
|
966
|
+
},
|
|
967
|
+
)
|
|
968
|
+
|
|
969
|
+
@field_validator("placed_time", mode="before")
|
|
970
|
+
@classmethod
|
|
971
|
+
def parse_placed_time(cls, value: object) -> datetime | None:
|
|
972
|
+
return parse_broker_datetime(value)
|
|
973
|
+
|
|
974
|
+
@field_validator("order_ids", mode="before")
|
|
975
|
+
@classmethod
|
|
976
|
+
def normalize_order_ids(cls, value: object) -> object:
|
|
977
|
+
return _normalize_response_ids(value, "orderId", "OrderId")
|
|
978
|
+
|
|
979
|
+
@field_validator("orders", mode="before")
|
|
980
|
+
@classmethod
|
|
981
|
+
def normalize_lists(cls, value: object) -> object:
|
|
982
|
+
if value is None:
|
|
983
|
+
return []
|
|
984
|
+
if isinstance(value, dict):
|
|
985
|
+
return [cast(dict[str, Any], value)]
|
|
986
|
+
return value
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
class CancelOrderResponse(BrokerModel):
|
|
990
|
+
account_id: str | None = Field(default=None, alias="accountId")
|
|
991
|
+
order_id: int | None = Field(default=None, alias="orderId")
|
|
992
|
+
cancel_time: datetime | None = Field(default=None, alias="cancelTime")
|
|
993
|
+
messages: Messages | None = None
|
|
994
|
+
|
|
995
|
+
@field_validator("cancel_time", mode="before")
|
|
996
|
+
@classmethod
|
|
997
|
+
def parse_cancel_time(cls, value: object) -> datetime | None:
|
|
998
|
+
return parse_broker_datetime(value)
|
|
999
|
+
|
|
1000
|
+
@model_validator(mode="before")
|
|
1001
|
+
@classmethod
|
|
1002
|
+
def normalize_response(cls, value: object) -> object:
|
|
1003
|
+
if not isinstance(value, dict):
|
|
1004
|
+
return value
|
|
1005
|
+
data = dict(cast(dict[str, Any], value))
|
|
1006
|
+
if "Messages" in data and "messages" not in data:
|
|
1007
|
+
data["messages"] = data["Messages"]
|
|
1008
|
+
data.pop("Messages", None)
|
|
1009
|
+
return data
|