architect-py 5.1.4b1__py3-none-any.whl → 5.1.5__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 (35) hide show
  1. architect_py/__init__.py +19 -1
  2. architect_py/async_client.py +158 -45
  3. architect_py/client.py +4 -0
  4. architect_py/client.pyi +33 -9
  5. architect_py/grpc/client.py +24 -9
  6. architect_py/grpc/models/AlgoHelper/AlgoParamTypes.py +31 -0
  7. architect_py/grpc/models/AlgoHelper/__init__.py +2 -0
  8. architect_py/grpc/models/Auth/AuthInfoRequest.py +37 -0
  9. architect_py/grpc/models/Auth/AuthInfoResponse.py +30 -0
  10. architect_py/grpc/models/Core/ConfigResponse.py +7 -2
  11. architect_py/grpc/models/Folio/AccountHistoryRequest.py +35 -4
  12. architect_py/grpc/models/Folio/AccountSummary.py +7 -1
  13. architect_py/grpc/models/Marketdata/Candle.py +6 -0
  14. architect_py/grpc/models/Marketdata/L1BookSnapshot.py +17 -3
  15. architect_py/grpc/models/Marketdata/L2BookSnapshot.py +6 -0
  16. architect_py/grpc/models/Marketdata/Liquidation.py +6 -0
  17. architect_py/grpc/models/Marketdata/Ticker.py +6 -0
  18. architect_py/grpc/models/Marketdata/Trade.py +6 -0
  19. architect_py/grpc/models/Oms/Order.py +38 -14
  20. architect_py/grpc/models/Oms/PlaceOrderRequest.py +38 -14
  21. architect_py/grpc/models/__init__.py +4 -1
  22. architect_py/grpc/models/definitions.py +173 -0
  23. architect_py/grpc/orderflow.py +138 -0
  24. architect_py/tests/test_order_entry.py +9 -6
  25. architect_py/tests/test_orderflow.py +116 -27
  26. architect_py/tests/test_positions.py +173 -0
  27. architect_py/tests/test_rounding.py +28 -28
  28. architect_py/utils/pandas.py +50 -1
  29. {architect_py-5.1.4b1.dist-info → architect_py-5.1.5.dist-info}/METADATA +1 -1
  30. {architect_py-5.1.4b1.dist-info → architect_py-5.1.5.dist-info}/RECORD +35 -29
  31. examples/funding_rate_mean_reversion_algo.py +23 -47
  32. scripts/postprocess_grpc.py +17 -2
  33. {architect_py-5.1.4b1.dist-info → architect_py-5.1.5.dist-info}/WHEEL +0 -0
  34. {architect_py-5.1.4b1.dist-info → architect_py-5.1.5.dist-info}/licenses/LICENSE +0 -0
  35. {architect_py-5.1.4b1.dist-info → architect_py-5.1.5.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,7 @@
1
1
  from types import UnionType
2
2
  from typing import Any, AsyncIterator, Sequence, Tuple
3
3
 
4
- import grpc
4
+ import grpc.aio
5
5
  import msgspec
6
6
 
7
7
  from . import *
@@ -17,6 +17,8 @@ class GrpcClient:
17
17
  endpoint: str
18
18
  channel: grpc.aio.Channel
19
19
  jwt: str | None = None
20
+ as_user: str | None = None
21
+ as_role: str | None = None
20
22
 
21
23
  def __init__(
22
24
  self,
@@ -25,9 +27,13 @@ class GrpcClient:
25
27
  port: int,
26
28
  use_ssl: bool = False,
27
29
  options: Sequence[Tuple[str, Any]] | None = None,
30
+ as_user: str | None = None,
31
+ as_role: str | None = None,
28
32
  ):
29
33
  scheme = "https" if use_ssl else "http"
30
34
  self.endpoint = f"{scheme}://{host}:{port}"
35
+ self.as_user = as_user
36
+ self.as_role = as_role
31
37
  if use_ssl:
32
38
  credentials = grpc.ssl_channel_credentials()
33
39
  self.channel = grpc.aio.secure_channel(
@@ -39,6 +45,16 @@ class GrpcClient:
39
45
  def set_jwt(self, jwt: str | None):
40
46
  self.jwt = jwt
41
47
 
48
+ def metadata(self) -> Sequence[Tuple[str, str]]:
49
+ metadata = []
50
+ if self.jwt is not None:
51
+ metadata.append(("authorization", f"Bearer {self.jwt}"))
52
+ if self.as_user is not None:
53
+ metadata.append(("x-architect-user", self.as_user))
54
+ if self.as_role is not None:
55
+ metadata.append(("x-architect-role", self.as_role))
56
+ return metadata
57
+
42
58
  async def close(self):
43
59
  await self.channel.close()
44
60
 
@@ -59,6 +75,8 @@ class GrpcClient:
59
75
  async def unary_unary(
60
76
  self,
61
77
  request: RequestType[ResponseTypeGeneric],
78
+ *,
79
+ no_metadata: bool = False,
62
80
  ) -> ResponseTypeGeneric:
63
81
  """
64
82
  Generic function for making a unary RPC call to the gRPC server.
@@ -75,10 +93,10 @@ class GrpcClient:
75
93
  request_serializer=encoder.encode,
76
94
  response_deserializer=decoder.decode,
77
95
  )
78
- if self.jwt is not None:
79
- metadata = (("authorization", f"Bearer {self.jwt}"),)
80
- else:
96
+ if no_metadata:
81
97
  metadata = ()
98
+ else:
99
+ metadata = self.metadata()
82
100
  return await stub(request, metadata=metadata)
83
101
 
84
102
  async def unary_stream(
@@ -100,11 +118,8 @@ class GrpcClient:
100
118
  request_serializer=encoder.encode,
101
119
  response_deserializer=decoder.decode,
102
120
  )
103
- if self.jwt is not None:
104
- metadata = (("authorization", f"Bearer {self.jwt}"),)
105
- else:
106
- metadata = ()
107
- call: grpc.aio._base_call.UnaryStreamCall[
121
+ metadata = self.metadata()
122
+ call: grpc.aio.UnaryStreamCall[
108
123
  RequestType[ResponseTypeGeneric], ResponseTypeGeneric
109
124
  ] = stub(request, metadata=metadata)
110
125
  async for update in call:
@@ -0,0 +1,31 @@
1
+ # generated by datamodel-codegen:
2
+ # filename: AlgoHelper/AlgoParamTypes.json
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import List, Union
7
+
8
+ from msgspec import Struct
9
+
10
+ from .. import definitions
11
+
12
+
13
+ class AlgoParamTypes(Struct, omit_defaults=True):
14
+ """
15
+ this is used to coerce creation of the params in the schema.json
16
+ """
17
+
18
+ spreader: List[Union[definitions.SpreaderParams, definitions.SpreaderStatus]]
19
+
20
+ # Constructor that takes all field titles as arguments for convenience
21
+ @classmethod
22
+ def new(
23
+ cls,
24
+ spreader: List[Union[definitions.SpreaderParams, definitions.SpreaderStatus]],
25
+ ):
26
+ return cls(
27
+ spreader,
28
+ )
29
+
30
+ def __str__(self) -> str:
31
+ return f"AlgoParamTypes(spreader={self.spreader})"
@@ -0,0 +1,2 @@
1
+ # generated by datamodel-codegen:
2
+ # filename: schemas
@@ -0,0 +1,37 @@
1
+ # generated by datamodel-codegen:
2
+ # filename: Auth/AuthInfoRequest.json
3
+
4
+ from __future__ import annotations
5
+ from architect_py.grpc.models.Auth.AuthInfoResponse import AuthInfoResponse
6
+
7
+ from msgspec import Struct
8
+
9
+
10
+ class AuthInfoRequest(Struct, omit_defaults=True):
11
+ pass
12
+
13
+ # Constructor that takes all field titles as arguments for convenience
14
+ @classmethod
15
+ def new(
16
+ cls,
17
+ ):
18
+ return cls()
19
+
20
+ def __str__(self) -> str:
21
+ return f"AuthInfoRequest()"
22
+
23
+ @staticmethod
24
+ def get_response_type():
25
+ return AuthInfoResponse
26
+
27
+ @staticmethod
28
+ def get_unannotated_response_type():
29
+ return AuthInfoResponse
30
+
31
+ @staticmethod
32
+ def get_route() -> str:
33
+ return "/json.architect.Auth/AuthInfo"
34
+
35
+ @staticmethod
36
+ def get_rpc_method():
37
+ return "unary"
@@ -0,0 +1,30 @@
1
+ # generated by datamodel-codegen:
2
+ # filename: Auth/AuthInfoResponse.json
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import Optional
7
+
8
+ from msgspec import Struct
9
+
10
+ from .. import definitions
11
+
12
+
13
+ class AuthInfoResponse(Struct, omit_defaults=True):
14
+ original_user_id: Optional[definitions.UserId] = None
15
+ user_id: Optional[definitions.UserId] = None
16
+
17
+ # Constructor that takes all field titles as arguments for convenience
18
+ @classmethod
19
+ def new(
20
+ cls,
21
+ original_user_id: Optional[definitions.UserId] = None,
22
+ user_id: Optional[definitions.UserId] = None,
23
+ ):
24
+ return cls(
25
+ original_user_id,
26
+ user_id,
27
+ )
28
+
29
+ def __str__(self) -> str:
30
+ return f"AuthInfoResponse(original_user_id={self.original_user_id},user_id={self.user_id})"
@@ -3,23 +3,28 @@
3
3
 
4
4
  from __future__ import annotations
5
5
 
6
- from typing import Dict
6
+ from typing import Dict, Optional
7
7
 
8
8
  from msgspec import Struct
9
9
 
10
10
 
11
11
  class ConfigResponse(Struct, omit_defaults=True):
12
12
  marketdata: Dict[str, str]
13
+ symbology: Optional[str] = None
13
14
 
14
15
  # Constructor that takes all field titles as arguments for convenience
15
16
  @classmethod
16
17
  def new(
17
18
  cls,
18
19
  marketdata: Dict[str, str],
20
+ symbology: Optional[str] = None,
19
21
  ):
20
22
  return cls(
21
23
  marketdata,
24
+ symbology,
22
25
  )
23
26
 
24
27
  def __str__(self) -> str:
25
- return f"ConfigResponse(marketdata={self.marketdata})"
28
+ return (
29
+ f"ConfigResponse(marketdata={self.marketdata},symbology={self.symbology})"
30
+ )
@@ -4,10 +4,10 @@
4
4
  from __future__ import annotations
5
5
  from architect_py.grpc.models.Folio.AccountHistoryResponse import AccountHistoryResponse
6
6
 
7
- from datetime import datetime
8
- from typing import Optional
7
+ from datetime import datetime, time
8
+ from typing import Annotated, Optional
9
9
 
10
- from msgspec import Struct
10
+ from msgspec import Meta, Struct
11
11
 
12
12
  from .. import definitions
13
13
 
@@ -15,6 +15,31 @@ from .. import definitions
15
15
  class AccountHistoryRequest(Struct, omit_defaults=True):
16
16
  account: definitions.AccountIdOrName
17
17
  from_inclusive: Optional[datetime] = None
18
+ granularity: Optional[definitions.AccountHistoryGranularity] = None
19
+ limit: Optional[
20
+ Annotated[
21
+ Optional[int],
22
+ Meta(
23
+ description="Default maximum of 100 data points. If the number of data points between from_inclusive and to_exclusive exceeds the limit, the response will be truncated. Data is always returned in descending timestamp order."
24
+ ),
25
+ ]
26
+ ] = None
27
+ """
28
+ Default maximum of 100 data points. If the number of data points between from_inclusive and to_exclusive exceeds the limit, the response will be truncated. Data is always returned in descending timestamp order.
29
+ """
30
+ time_of_day: Optional[
31
+ Annotated[
32
+ Optional[time],
33
+ Meta(
34
+ description="For daily granularity, the UTC time of day to use for each day.\n\nCurrently the seconds and subseconds parts are ignored."
35
+ ),
36
+ ]
37
+ ] = None
38
+ """
39
+ For daily granularity, the UTC time of day to use for each day.
40
+
41
+ Currently the seconds and subseconds parts are ignored.
42
+ """
18
43
  to_exclusive: Optional[datetime] = None
19
44
 
20
45
  # Constructor that takes all field titles as arguments for convenience
@@ -23,16 +48,22 @@ class AccountHistoryRequest(Struct, omit_defaults=True):
23
48
  cls,
24
49
  account: definitions.AccountIdOrName,
25
50
  from_inclusive: Optional[datetime] = None,
51
+ granularity: Optional[definitions.AccountHistoryGranularity] = None,
52
+ limit: Optional[int] = None,
53
+ time_of_day: Optional[time] = None,
26
54
  to_exclusive: Optional[datetime] = None,
27
55
  ):
28
56
  return cls(
29
57
  account,
30
58
  from_inclusive,
59
+ granularity,
60
+ limit,
61
+ time_of_day,
31
62
  to_exclusive,
32
63
  )
33
64
 
34
65
  def __str__(self) -> str:
35
- return f"AccountHistoryRequest(account={self.account},from_inclusive={self.from_inclusive},to_exclusive={self.to_exclusive})"
66
+ return f"AccountHistoryRequest(account={self.account},from_inclusive={self.from_inclusive},granularity={self.granularity},limit={self.limit},time_of_day={self.time_of_day},to_exclusive={self.to_exclusive})"
36
67
 
37
68
  @staticmethod
38
69
  def get_response_type():
@@ -15,7 +15,13 @@ from .. import definitions
15
15
  class AccountSummary(Struct, omit_defaults=True):
16
16
  account: str
17
17
  balances: Dict[str, Decimal]
18
- positions: Dict[str, List[definitions.AccountPosition]]
18
+ positions: Annotated[
19
+ Dict[str, List[definitions.AccountPosition]],
20
+ Meta(description="map from TradableProduct to a list of AccountPosition"),
21
+ ]
22
+ """
23
+ map from TradableProduct to a list of AccountPosition
24
+ """
19
25
  timestamp: datetime
20
26
  cash_excess: Optional[
21
27
  Annotated[Optional[Decimal], Meta(description="Cash available to withdraw.")]
@@ -136,10 +136,16 @@ class Candle(Struct, omit_defaults=True):
136
136
 
137
137
  @property
138
138
  def datetime(self) -> datetime:
139
+ """
140
+ Convenience property to get the timestamp as a datetime object in UTC.
141
+ """
139
142
  return datetime.fromtimestamp(self.ts, tz=timezone.utc)
140
143
 
141
144
  @property
142
145
  def datetime_local(self) -> datetime:
146
+ """
147
+ Convenience property to get the timestamp as a datetime object in local time.
148
+ """
143
149
  return datetime.fromtimestamp(self.ts)
144
150
 
145
151
  @property
@@ -13,7 +13,15 @@ from msgspec import Meta, Struct
13
13
  class L1BookSnapshot(Struct, omit_defaults=True):
14
14
  s: Annotated[str, Meta(title="symbol")]
15
15
  tn: Annotated[int, Meta(ge=0, title="timestamp_ns")]
16
- ts: Annotated[int, Meta(title="timestamp")]
16
+ ts: Annotated[
17
+ int,
18
+ Meta(
19
+ description="Time that the exchange stamped the message", title="timestamp"
20
+ ),
21
+ ]
22
+ """
23
+ Time that the exchange stamped the message
24
+ """
17
25
  a: Optional[
18
26
  Annotated[
19
27
  List[Decimal], Meta(description="(price, quantity)", title="best_ask")
@@ -34,13 +42,13 @@ class L1BookSnapshot(Struct, omit_defaults=True):
34
42
  Annotated[
35
43
  Optional[int],
36
44
  Meta(
37
- description="Time that Architect feed received the message; only set if streaming from direct L1 feeds",
45
+ description="Time that Architect feed received the message that updated the BBO",
38
46
  title="recv_time",
39
47
  ),
40
48
  ]
41
49
  ] = None
42
50
  """
43
- Time that Architect feed received the message; only set if streaming from direct L1 feeds
51
+ Time that Architect feed received the message that updated the BBO
44
52
  """
45
53
  rtn: Optional[Annotated[Optional[int], Meta(title="recv_time_ns")]] = None
46
54
 
@@ -95,10 +103,16 @@ class L1BookSnapshot(Struct, omit_defaults=True):
95
103
 
96
104
  @property
97
105
  def datetime(self) -> datetime:
106
+ """
107
+ Convenience property to get the timestamp as a datetime object in UTC.
108
+ """
98
109
  return datetime.fromtimestamp(self.ts, tz=timezone.utc)
99
110
 
100
111
  @property
101
112
  def datetime_local(self) -> datetime:
113
+ """
114
+ Convenience property to get the timestamp as a datetime object in local time.
115
+ """
102
116
  return datetime.fromtimestamp(self.ts)
103
117
 
104
118
  @property
@@ -100,8 +100,14 @@ class L2BookSnapshot(Struct, omit_defaults=True):
100
100
 
101
101
  @property
102
102
  def datetime(self) -> datetime:
103
+ """
104
+ Convenience property to get the timestamp as a datetime object in UTC.
105
+ """
103
106
  return datetime.fromtimestamp(self.ts, tz=timezone.utc)
104
107
 
105
108
  @property
106
109
  def datetime_local(self) -> datetime:
110
+ """
111
+ Convenience property to get the timestamp as a datetime object in local time.
112
+ """
107
113
  return datetime.fromtimestamp(self.ts)
@@ -93,8 +93,14 @@ class Liquidation(Struct, omit_defaults=True):
93
93
 
94
94
  @property
95
95
  def datetime(self) -> datetime:
96
+ """
97
+ Convenience property to get the timestamp as a datetime object in UTC.
98
+ """
96
99
  return datetime.fromtimestamp(self.ts, tz=timezone.utc)
97
100
 
98
101
  @property
99
102
  def datetime_local(self) -> datetime:
103
+ """
104
+ Convenience property to get the timestamp as a datetime object in local time.
105
+ """
100
106
  return datetime.fromtimestamp(self.ts)
@@ -155,10 +155,16 @@ class Ticker(Struct, omit_defaults=True):
155
155
 
156
156
  @property
157
157
  def datetime(self) -> datetime:
158
+ """
159
+ Convenience property to get the timestamp as a datetime object in UTC.
160
+ """
158
161
  return datetime.fromtimestamp(self.ts, tz=timezone.utc)
159
162
 
160
163
  @property
161
164
  def datetime_local(self) -> datetime:
165
+ """
166
+ Convenience property to get the timestamp as a datetime object in local time.
167
+ """
162
168
  return datetime.fromtimestamp(self.ts)
163
169
 
164
170
  @property
@@ -85,10 +85,16 @@ class Trade(Struct, omit_defaults=True):
85
85
 
86
86
  @property
87
87
  def datetime(self) -> datetime:
88
+ """
89
+ Convenience property to get the timestamp as a datetime object in UTC.
90
+ """
88
91
  return datetime.fromtimestamp(self.ts, tz=timezone.utc)
89
92
 
90
93
  @property
91
94
  def datetime_local(self) -> datetime:
95
+ """
96
+ Convenience property to get the timestamp as a datetime object in local time.
97
+ """
92
98
  return datetime.fromtimestamp(self.ts)
93
99
 
94
100
  @property
@@ -51,6 +51,10 @@ class Order(Struct, omit_defaults=True):
51
51
  p: Optional[Annotated[Decimal, Meta(title="limit_price")]] = None
52
52
  po: Optional[Annotated[bool, Meta(title="post_only")]] = None
53
53
  tp: Optional[Annotated[Decimal, Meta(title="trigger_price")]] = None
54
+ sl: Optional[
55
+ Annotated[Optional[definitions.TriggerLimitOrderType], Meta(title="stop_loss")]
56
+ ] = None
57
+ tpp: Optional[Annotated[Optional[Decimal], Meta(title="take_profit_price")]] = None
54
58
 
55
59
  # Constructor that takes all field titles as arguments for convenience
56
60
  @classmethod
@@ -79,6 +83,8 @@ class Order(Struct, omit_defaults=True):
79
83
  limit_price: Optional[Decimal] = None,
80
84
  post_only: Optional[bool] = None,
81
85
  trigger_price: Optional[Decimal] = None,
86
+ stop_loss: Optional[definitions.TriggerLimitOrderType] = None,
87
+ take_profit_price: Optional[Decimal] = None,
82
88
  ):
83
89
  return cls(
84
90
  account,
@@ -104,10 +110,12 @@ class Order(Struct, omit_defaults=True):
104
110
  limit_price,
105
111
  post_only,
106
112
  trigger_price,
113
+ stop_loss,
114
+ take_profit_price,
107
115
  )
108
116
 
109
117
  def __str__(self) -> str:
110
- return f"Order(account={self.a},dir={self.d},id={self.id},status={self.o},quantity={self.q},symbol={self.s},source={self.src},time_in_force={self.tif},recv_time_ns={self.tn},recv_time={self.ts},trader={self.u},execution_venue={self.ve},filled_quantity={self.xq},order_type={self.k},exchange_order_id={self.eid},parent_id={self.pid},reject_reason={self.r},reject_message={self.rm},is_short_sale={self.ss},average_fill_price={self.xp},limit_price={self.p},post_only={self.po},trigger_price={self.tp})"
118
+ return f"Order(account={self.a},dir={self.d},id={self.id},status={self.o},quantity={self.q},symbol={self.s},source={self.src},time_in_force={self.tif},recv_time_ns={self.tn},recv_time={self.ts},trader={self.u},execution_venue={self.ve},filled_quantity={self.xq},order_type={self.k},exchange_order_id={self.eid},parent_id={self.pid},reject_reason={self.r},reject_message={self.rm},is_short_sale={self.ss},average_fill_price={self.xp},limit_price={self.p},post_only={self.po},trigger_price={self.tp},stop_loss={self.sl},take_profit_price={self.tpp})"
111
119
 
112
120
  @property
113
121
  def account(self) -> str:
@@ -285,20 +293,27 @@ class Order(Struct, omit_defaults=True):
285
293
  def trigger_price(self, value: Optional[Decimal]) -> None:
286
294
  self.tp = value
287
295
 
296
+ @property
297
+ def stop_loss(self) -> Optional[definitions.TriggerLimitOrderType]:
298
+ return self.sl
299
+
300
+ @stop_loss.setter
301
+ def stop_loss(self, value: Optional[definitions.TriggerLimitOrderType]) -> None:
302
+ self.sl = value
303
+
304
+ @property
305
+ def take_profit_price(self) -> Optional[Decimal]:
306
+ return self.tpp
307
+
308
+ @take_profit_price.setter
309
+ def take_profit_price(self, value: Optional[Decimal]) -> None:
310
+ self.tpp = value
311
+
288
312
  def __post_init__(self):
289
- if self.k == "MARKET":
290
- if not all(getattr(self, key) is not None for key in []):
291
- raise ValueError(
292
- f"When field k (order_type) is of value MARKET, class Order requires fields []"
293
- )
294
- elif any(getattr(self, key) is not None for key in ["p", "po", "tp"]):
295
- raise ValueError(
296
- f"When field k (order_type) is of value MARKET, class Order should not have fields ['p', 'po', 'tp']"
297
- )
298
- elif self.k == "LIMIT":
313
+ if self.k == "LIMIT":
299
314
  if not all(getattr(self, key) is not None for key in ["p", "po"]):
300
315
  raise ValueError(
301
- f"When field k (order_type) is of value LIMIT, class Order requires fields ['p', 'po']"
316
+ f"When field k (order_type) is of value LIMIT, class Order requires fields ['limit_price (p)', 'post_only (po)']"
302
317
  )
303
318
  elif any(getattr(self, key) is not None for key in ["tp"]):
304
319
  raise ValueError(
@@ -307,7 +322,7 @@ class Order(Struct, omit_defaults=True):
307
322
  elif self.k == "STOP_LOSS_LIMIT":
308
323
  if not all(getattr(self, key) is not None for key in ["p", "tp"]):
309
324
  raise ValueError(
310
- f"When field k (order_type) is of value STOP_LOSS_LIMIT, class Order requires fields ['p', 'tp']"
325
+ f"When field k (order_type) is of value STOP_LOSS_LIMIT, class Order requires fields ['limit_price (p)', 'trigger_price (tp)']"
311
326
  )
312
327
  elif any(getattr(self, key) is not None for key in ["po"]):
313
328
  raise ValueError(
@@ -316,9 +331,18 @@ class Order(Struct, omit_defaults=True):
316
331
  elif self.k == "TAKE_PROFIT_LIMIT":
317
332
  if not all(getattr(self, key) is not None for key in ["p", "tp"]):
318
333
  raise ValueError(
319
- f"When field k (order_type) is of value TAKE_PROFIT_LIMIT, class Order requires fields ['p', 'tp']"
334
+ f"When field k (order_type) is of value TAKE_PROFIT_LIMIT, class Order requires fields ['limit_price (p)', 'trigger_price (tp)']"
320
335
  )
321
336
  elif any(getattr(self, key) is not None for key in ["po"]):
322
337
  raise ValueError(
323
338
  f"When field k (order_type) is of value TAKE_PROFIT_LIMIT, class Order should not have fields ['po']"
324
339
  )
340
+ elif self.k == "BRACKET":
341
+ if not all(getattr(self, key) is not None for key in ["p", "po"]):
342
+ raise ValueError(
343
+ f"When field k (order_type) is of value BRACKET, class Order requires fields ['limit_price (p)', 'post_only (po)']"
344
+ )
345
+ elif any(getattr(self, key) is not None for key in ["tp"]):
346
+ raise ValueError(
347
+ f"When field k (order_type) is of value BRACKET, class Order should not have fields ['tp']"
348
+ )
@@ -46,6 +46,10 @@ class PlaceOrderRequest(Struct, omit_defaults=True):
46
46
  p: Optional[Annotated[Decimal, Meta(title="limit_price")]] = None
47
47
  po: Optional[Annotated[bool, Meta(title="post_only")]] = None
48
48
  tp: Optional[Annotated[Decimal, Meta(title="trigger_price")]] = None
49
+ sl: Optional[
50
+ Annotated[Optional[definitions.TriggerLimitOrderType], Meta(title="stop_loss")]
51
+ ] = None
52
+ tpp: Optional[Annotated[Optional[Decimal], Meta(title="take_profit_price")]] = None
49
53
 
50
54
  # Constructor that takes all field titles as arguments for convenience
51
55
  @classmethod
@@ -65,6 +69,8 @@ class PlaceOrderRequest(Struct, omit_defaults=True):
65
69
  limit_price: Optional[Decimal] = None,
66
70
  post_only: Optional[bool] = None,
67
71
  trigger_price: Optional[Decimal] = None,
72
+ stop_loss: Optional[definitions.TriggerLimitOrderType] = None,
73
+ take_profit_price: Optional[Decimal] = None,
68
74
  ):
69
75
  return cls(
70
76
  dir,
@@ -81,10 +87,12 @@ class PlaceOrderRequest(Struct, omit_defaults=True):
81
87
  limit_price,
82
88
  post_only,
83
89
  trigger_price,
90
+ stop_loss,
91
+ take_profit_price,
84
92
  )
85
93
 
86
94
  def __str__(self) -> str:
87
- return f"PlaceOrderRequest(dir={self.d},quantity={self.q},symbol={self.s},time_in_force={self.tif},order_type={self.k},account={self.a},id={self.id},parent_id={self.pid},source={self.src},trader={self.u},execution_venue={self.x},limit_price={self.p},post_only={self.po},trigger_price={self.tp})"
95
+ return f"PlaceOrderRequest(dir={self.d},quantity={self.q},symbol={self.s},time_in_force={self.tif},order_type={self.k},account={self.a},id={self.id},parent_id={self.pid},source={self.src},trader={self.u},execution_venue={self.x},limit_price={self.p},post_only={self.po},trigger_price={self.tp},stop_loss={self.sl},take_profit_price={self.tpp})"
88
96
 
89
97
  @property
90
98
  def dir(self) -> OrderDir:
@@ -190,6 +198,22 @@ class PlaceOrderRequest(Struct, omit_defaults=True):
190
198
  def trigger_price(self, value: Optional[Decimal]) -> None:
191
199
  self.tp = value
192
200
 
201
+ @property
202
+ def stop_loss(self) -> Optional[definitions.TriggerLimitOrderType]:
203
+ return self.sl
204
+
205
+ @stop_loss.setter
206
+ def stop_loss(self, value: Optional[definitions.TriggerLimitOrderType]) -> None:
207
+ self.sl = value
208
+
209
+ @property
210
+ def take_profit_price(self) -> Optional[Decimal]:
211
+ return self.tpp
212
+
213
+ @take_profit_price.setter
214
+ def take_profit_price(self, value: Optional[Decimal]) -> None:
215
+ self.tpp = value
216
+
193
217
  @staticmethod
194
218
  def get_response_type():
195
219
  return Order
@@ -207,19 +231,10 @@ class PlaceOrderRequest(Struct, omit_defaults=True):
207
231
  return "unary"
208
232
 
209
233
  def __post_init__(self):
210
- if self.k == "MARKET":
211
- if not all(getattr(self, key) is not None for key in []):
212
- raise ValueError(
213
- f"When field k (order_type) is of value MARKET, class PlaceOrderRequest requires fields []"
214
- )
215
- elif any(getattr(self, key) is not None for key in ["p", "po", "tp"]):
216
- raise ValueError(
217
- f"When field k (order_type) is of value MARKET, class PlaceOrderRequest should not have fields ['p', 'po', 'tp']"
218
- )
219
- elif self.k == "LIMIT":
234
+ if self.k == "LIMIT":
220
235
  if not all(getattr(self, key) is not None for key in ["p", "po"]):
221
236
  raise ValueError(
222
- f"When field k (order_type) is of value LIMIT, class PlaceOrderRequest requires fields ['p', 'po']"
237
+ f"When field k (order_type) is of value LIMIT, class PlaceOrderRequest requires fields ['limit_price (p)', 'post_only (po)']"
223
238
  )
224
239
  elif any(getattr(self, key) is not None for key in ["tp"]):
225
240
  raise ValueError(
@@ -228,7 +243,7 @@ class PlaceOrderRequest(Struct, omit_defaults=True):
228
243
  elif self.k == "STOP_LOSS_LIMIT":
229
244
  if not all(getattr(self, key) is not None for key in ["p", "tp"]):
230
245
  raise ValueError(
231
- f"When field k (order_type) is of value STOP_LOSS_LIMIT, class PlaceOrderRequest requires fields ['p', 'tp']"
246
+ f"When field k (order_type) is of value STOP_LOSS_LIMIT, class PlaceOrderRequest requires fields ['limit_price (p)', 'trigger_price (tp)']"
232
247
  )
233
248
  elif any(getattr(self, key) is not None for key in ["po"]):
234
249
  raise ValueError(
@@ -237,9 +252,18 @@ class PlaceOrderRequest(Struct, omit_defaults=True):
237
252
  elif self.k == "TAKE_PROFIT_LIMIT":
238
253
  if not all(getattr(self, key) is not None for key in ["p", "tp"]):
239
254
  raise ValueError(
240
- f"When field k (order_type) is of value TAKE_PROFIT_LIMIT, class PlaceOrderRequest requires fields ['p', 'tp']"
255
+ f"When field k (order_type) is of value TAKE_PROFIT_LIMIT, class PlaceOrderRequest requires fields ['limit_price (p)', 'trigger_price (tp)']"
241
256
  )
242
257
  elif any(getattr(self, key) is not None for key in ["po"]):
243
258
  raise ValueError(
244
259
  f"When field k (order_type) is of value TAKE_PROFIT_LIMIT, class PlaceOrderRequest should not have fields ['po']"
245
260
  )
261
+ elif self.k == "BRACKET":
262
+ if not all(getattr(self, key) is not None for key in ["p", "po"]):
263
+ raise ValueError(
264
+ f"When field k (order_type) is of value BRACKET, class PlaceOrderRequest requires fields ['limit_price (p)', 'post_only (po)']"
265
+ )
266
+ elif any(getattr(self, key) is not None for key in ["tp"]):
267
+ raise ValueError(
268
+ f"When field k (order_type) is of value BRACKET, class PlaceOrderRequest should not have fields ['tp']"
269
+ )