architect-py 5.1.0b1__py3-none-any.whl → 5.1.2__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 +3 -1
  2. architect_py/async_client.py +116 -28
  3. architect_py/client.py +56 -70
  4. architect_py/client.pyi +54 -12
  5. architect_py/common_types/time_in_force.py +1 -0
  6. architect_py/common_types/tradable_product.py +1 -1
  7. architect_py/graphql_client/juniper_base_client.py +4 -0
  8. architect_py/grpc/client.py +3 -0
  9. architect_py/grpc/models/Cpty/CptyStatus.py +17 -18
  10. architect_py/grpc/models/Marketdata/Ticker.py +14 -1
  11. architect_py/grpc/models/Oms/Order.py +12 -1
  12. architect_py/grpc/models/definitions.py +41 -0
  13. architect_py/grpc/resolve_endpoint.py +11 -2
  14. architect_py/tests/conftest.py +2 -5
  15. architect_py/tests/test_encoding.py +37 -0
  16. architect_py/tests/test_marketdata.py +9 -0
  17. architect_py/tests/test_order_entry.py +3 -1
  18. architect_py/tests/test_portfolio_management.py +2 -0
  19. architect_py/tests/test_symbology.py +20 -0
  20. architect_py/tests/test_sync_client.py +40 -0
  21. {architect_py-5.1.0b1.dist-info → architect_py-5.1.2.dist-info}/METADATA +4 -1
  22. {architect_py-5.1.0b1.dist-info → architect_py-5.1.2.dist-info}/RECORD +35 -33
  23. {architect_py-5.1.0b1.dist-info → architect_py-5.1.2.dist-info}/WHEEL +1 -1
  24. examples/book_subscription.py +2 -0
  25. examples/candles.py +2 -0
  26. examples/funding_rate_mean_reversion_algo.py +1 -0
  27. examples/order_sending.py +5 -3
  28. examples/stream_l1_marketdata.py +2 -0
  29. examples/stream_l2_marketdata.py +2 -0
  30. examples/trades.py +2 -0
  31. examples/tutorial_async.py +3 -1
  32. examples/tutorial_sync.py +4 -1
  33. templates/juniper_base_client.py +4 -0
  34. {architect_py-5.1.0b1.dist-info → architect_py-5.1.2.dist-info}/licenses/LICENSE +0 -0
  35. {architect_py-5.1.0b1.dist-info → architect_py-5.1.2.dist-info}/top_level.txt +0 -0
architect_py/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  # ruff: noqa:I001
2
2
 
3
- __version__ = "5.1.0b1"
3
+ __version__ = "5.1.2"
4
4
 
5
5
  from .utils.nearest_tick import TickRoundMethod
6
6
  from .async_client import AsyncClient
@@ -13,6 +13,7 @@ from .grpc.models.definitions import (
13
13
  AlgoOrderStatus,
14
14
  CancelStatus,
15
15
  CandleWidth,
16
+ ConnectionStatus,
16
17
  CptyLogoutRequest,
17
18
  DateTimeOrUtc,
18
19
  Deposit,
@@ -269,6 +270,7 @@ __all__ = [
269
270
  "Commodity",
270
271
  "ConfigRequest",
271
272
  "ConfigResponse",
273
+ "ConnectionStatus",
272
274
  "CptyLoginRequest",
273
275
  "CptyLogoutRequest",
274
276
  "CptyRequest",
@@ -43,7 +43,7 @@ from architect_py.grpc.models.Orderflow.OrderflowRequest import (
43
43
  OrderflowRequest_route,
44
44
  OrderflowRequestUnannotatedResponseType,
45
45
  )
46
- from architect_py.grpc.resolve_endpoint import resolve_endpoint
46
+ from architect_py.grpc.resolve_endpoint import PAPER_GRPC_PORT, resolve_endpoint
47
47
  from architect_py.utils.nearest_tick import TickRoundMethod
48
48
  from architect_py.utils.orderbook import update_orderbook_side
49
49
  from architect_py.utils.pandas import candles_to_dataframe
@@ -102,7 +102,9 @@ class AsyncClient:
102
102
  )
103
103
  endpoint = kwargs["endpoint"]
104
104
 
105
- grpc_host, grpc_port, use_ssl = await resolve_endpoint(endpoint)
105
+ grpc_host, grpc_port, use_ssl = await resolve_endpoint(
106
+ endpoint, paper_trading=paper_trading
107
+ )
106
108
  logging.info(
107
109
  f"Resolved endpoint {endpoint}: {grpc_host}:{grpc_port} use_ssl={use_ssl}"
108
110
  )
@@ -110,7 +112,7 @@ class AsyncClient:
110
112
  # Sanity check paper trading on prod environments
111
113
  if paper_trading:
112
114
  if grpc_host == "app.architect.co" or grpc_host == "staging.architect.co":
113
- if grpc_port != 10081:
115
+ if grpc_port != PAPER_GRPC_PORT:
114
116
  raise ValueError("Wrong gRPC port for paper trading")
115
117
  if graphql_port is not None and graphql_port != 5678:
116
118
  raise ValueError("Wrong GraphQL port for paper trading")
@@ -179,6 +181,29 @@ class AsyncClient:
179
181
  )
180
182
  self.grpc_core = GrpcClient(host=grpc_host, port=grpc_port, use_ssl=use_ssl)
181
183
 
184
+ async def close(self):
185
+ """
186
+ Close the gRPC channel and GraphQL client.
187
+
188
+ This fixes the:
189
+ Error in sys.excepthook:
190
+
191
+ Original exception was:
192
+
193
+ One might get when closing the client
194
+ """
195
+ if self.grpc_core is not None:
196
+ await self.grpc_core.close()
197
+
198
+ for grpc_client in self.grpc_marketdata.values():
199
+ await grpc_client.close()
200
+
201
+ self.grpc_marketdata.clear()
202
+ # NB: this line removes the "Error in sys.excepthook:" on close
203
+
204
+ if self.graphql_client is not None:
205
+ await self.graphql_client.close()
206
+
182
207
  async def refresh_jwt(self, force: bool = False):
183
208
  """
184
209
  Refresh the JWT for the gRPC channel if it's nearing expiration (within 1 minute).
@@ -259,6 +284,9 @@ class AsyncClient:
259
284
  self.grpc_marketdata[venue] = GrpcClient(
260
285
  host=grpc_host, port=grpc_port, use_ssl=use_ssl
261
286
  )
287
+ logging.debug(
288
+ f"Setting marketdata endpoint for {venue}: {grpc_host}:{grpc_port} use_ssl={use_ssl}"
289
+ )
262
290
  except Exception as e:
263
291
  logging.error("Failed to set marketdata endpoint: %s", e)
264
292
 
@@ -503,34 +531,51 @@ class AsyncClient:
503
531
  if not series_symbol.endswith("Futures"):
504
532
  raise ValueError("series_symbol must end with 'Futures'")
505
533
  res = await self.graphql_client.get_future_series_query(series_symbol)
506
- return res.futures_series
534
+
535
+ today = date.today()
536
+
537
+ futures = [
538
+ future
539
+ for future in res.futures_series
540
+ if (exp := nominative_expiration(future)) is not None and exp > today
541
+ ]
542
+ futures.sort()
543
+
544
+ return futures
507
545
 
508
546
  async def get_front_future(
509
- self, series_symbol: str, venue: str, by_volume: bool = True
510
- ) -> str:
547
+ self, series_symbol: str, venue: Optional[str] = None
548
+ ) -> TradableProduct:
511
549
  """
512
- Gets the future with the most volume in a series.
550
+ Gets the front future.
551
+ ** If the venue is provided, it will return the future with the most volume in that venue**
552
+ Otherwise, will sort by expiration date and return the earliest future.
553
+
554
+ ** Note that this function returns a TradableProduct (ie with a base and a quote)
555
+
513
556
 
514
557
  Args:
515
558
  series_symbol: the futures series
516
559
  e.g. "ES CME Futures" would yield the lead future for the ES series
517
560
  venue: the venue to get the lead future for, e.g. "CME"
518
- by_volume: if True, sort by volume; otherwise sort by expiration date
561
+ ** If the venue is provided, it will return the future with the most volume in that venue**
519
562
 
520
563
  Returns:
521
564
  The lead future symbol
522
565
  """
523
566
  futures = await self.get_futures_series(series_symbol)
524
- if not by_volume:
567
+ if not venue:
525
568
  futures.sort()
526
- return futures[0]
569
+ return TradableProduct(futures[0], "USD")
527
570
  else:
528
571
  grpc_client = await self.marketdata(venue)
529
572
  req = TickersRequest(
530
- symbols=futures, k=SortTickersBy.VOLUME_DESC, venue=venue
573
+ symbols=[TradableProduct(f"{future}/USD") for future in futures],
574
+ k=SortTickersBy.VOLUME_DESC,
575
+ venue=venue,
531
576
  )
532
577
  res: TickersResponse = await grpc_client.unary_unary(req)
533
- return res.tickers[0].symbol
578
+ return TradableProduct(res.tickers[0].symbol)
534
579
 
535
580
  @staticmethod
536
581
  def get_expiration_from_CME_name(name: str) -> Optional[date]:
@@ -1337,8 +1382,7 @@ class AsyncClient:
1337
1382
 
1338
1383
  Example:
1339
1384
  ```python
1340
- request = SubscribeOrderflowRequest.new()
1341
- async for of in client.subscribe_orderflow_stream(request):
1385
+ async for of in client.stream_orderflow(account, execution_venue, trader):
1342
1386
  print(of)
1343
1387
  ```
1344
1388
  """
@@ -1375,6 +1419,48 @@ class AsyncClient:
1375
1419
  """
1376
1420
  return await self.place_limit_order(*args, **kwargs)
1377
1421
 
1422
+ async def place_orders(
1423
+ self, order_requests: Sequence[PlaceOrderRequest]
1424
+ ) -> list[Order]:
1425
+ """
1426
+ A low level function to place multiple orders in a single function.
1427
+
1428
+ This function does NOT check the validity of the parameters, so it is the user's responsibility
1429
+ to ensure that the orders are valid and will not be rejected by the OMS.
1430
+
1431
+ Args:
1432
+ order_request: the PlaceOrderRequest containing the orders to place
1433
+
1434
+
1435
+ Example of a PlaceOrderRequest:
1436
+ order_request: PlaceOrderRequest = PlaceOrderRequest.new(
1437
+ dir=dir,
1438
+ quantity=quantity,
1439
+ symbol=symbol,
1440
+ time_in_force=time_in_force,
1441
+ limit_price=limit_price,
1442
+ order_type=order_type,
1443
+ account=account,
1444
+ id=id,
1445
+ parent_id=None,
1446
+ source=OrderSource.API,
1447
+ trader=trader,
1448
+ execution_venue=execution_venue,
1449
+ post_only=post_only,
1450
+ trigger_price=trigger_price,
1451
+ )
1452
+ """
1453
+ grpc_client = await self.core()
1454
+
1455
+ res = await asyncio.gather(
1456
+ *[
1457
+ grpc_client.unary_unary(order_request)
1458
+ for order_request in order_requests
1459
+ ]
1460
+ )
1461
+
1462
+ return res
1463
+
1378
1464
  async def place_limit_order(
1379
1465
  self,
1380
1466
  *,
@@ -1448,7 +1534,7 @@ class AsyncClient:
1448
1534
  )
1449
1535
  if execution_info is None:
1450
1536
  raise ValueError(
1451
- "Could not find execution information for {symbol} for rounding price for limit order. Please round price manually."
1537
+ f"Could not find execution information for {symbol} for rounding price for limit order. Please round price manually."
1452
1538
  )
1453
1539
  if (tick_size := execution_info.tick_size) is not None:
1454
1540
  if tick_size:
@@ -1481,7 +1567,7 @@ class AsyncClient:
1481
1567
  id: Optional[OrderId] = None,
1482
1568
  symbol: TradableProduct | str,
1483
1569
  execution_venue: str,
1484
- odir: OrderDir,
1570
+ dir: OrderDir,
1485
1571
  quantity: Decimal,
1486
1572
  time_in_force: TimeInForce = TimeInForce.DAY,
1487
1573
  account: Optional[str] = None,
@@ -1495,7 +1581,7 @@ class AsyncClient:
1495
1581
  id: in case user wants to generate their own order id, otherwise it will be generated automatically
1496
1582
  symbol: the symbol to send the order for
1497
1583
  execution_venue: the execution venue to send the order to
1498
- odir: the direction of the order
1584
+ dir: the direction of the order
1499
1585
  quantity: the quantity of the order
1500
1586
  time_in_force: the time in force of the order
1501
1587
  account: the account to send the order for
@@ -1516,7 +1602,7 @@ class AsyncClient:
1516
1602
 
1517
1603
  price_band = price_band_pairs.get(symbol, None)
1518
1604
 
1519
- if odir == OrderDir.BUY:
1605
+ if dir == OrderDir.BUY:
1520
1606
  if ticker.ask_price is None:
1521
1607
  raise ValueError(
1522
1608
  f"Failed to send market order with reason: no ask price for {symbol}"
@@ -1539,7 +1625,7 @@ class AsyncClient:
1539
1625
 
1540
1626
  # Conservatively round price to nearest tick
1541
1627
  tick_round_method = (
1542
- TickRoundMethod.FLOOR if odir == OrderDir.BUY else TickRoundMethod.CEIL
1628
+ TickRoundMethod.FLOOR if dir == OrderDir.BUY else TickRoundMethod.CEIL
1543
1629
  )
1544
1630
 
1545
1631
  execution_info = await self.get_execution_info(
@@ -1556,7 +1642,7 @@ class AsyncClient:
1556
1642
  id=id,
1557
1643
  symbol=symbol,
1558
1644
  execution_venue=execution_venue,
1559
- odir=odir,
1645
+ dir=dir,
1560
1646
  quantity=quantity,
1561
1647
  account=account,
1562
1648
  order_type=OrderType.LIMIT,
@@ -1605,11 +1691,13 @@ class AsyncClient:
1605
1691
  if cancel.reject_reason is not None:
1606
1692
  return False
1607
1693
  return True
1608
- grpc_client = await self.core()
1609
- req = CancelAllOrdersRequest(
1610
- account=account,
1611
- execution_venue=execution_venue,
1612
- trader=trader,
1613
- )
1614
- res = await grpc_client.unary_unary(req)
1615
- return res
1694
+ # grpc_client = await self.core()
1695
+
1696
+ # req = CancelAllOrdersRequest(
1697
+ # id=str(uuid.uuid4()), # Unique ID for the request
1698
+ # account=account,
1699
+ # execution_venue=execution_venue,
1700
+ # trader=trader,
1701
+ # )
1702
+ # res = await grpc_client.unary_unary(req)
1703
+ # return True
architect_py/client.py CHANGED
@@ -2,13 +2,12 @@ import asyncio
2
2
  import sys
3
3
  import threading
4
4
  from asyncio import AbstractEventLoop
5
+ from collections.abc import Callable
5
6
  from functools import partial
6
- from typing import Any, Awaitable, Callable, Coroutine, Optional, TypeVar
7
+ from typing import Any, Concatenate, Coroutine, Optional, ParamSpec, TypeVar
7
8
 
8
9
  from .async_client import AsyncClient
9
10
 
10
- T = TypeVar("T")
11
-
12
11
 
13
12
  def is_async_function(obj):
14
13
  # can be converted to C function for faster performance
@@ -16,24 +15,30 @@ def is_async_function(obj):
16
15
  return callable(obj) and hasattr(obj, "__code__") and obj.__code__.co_flags & 0x80
17
16
 
18
17
 
18
+ P = ParamSpec("P")
19
+ T = TypeVar("T")
20
+
21
+
19
22
  class Client:
20
23
  """
24
+ One can find the function definition in the AsyncClient class and in the pyi file.
25
+
21
26
  This class is a wrapper around the AsyncClient class that allows you to call async methods synchronously.
22
27
  This does not work for subscription based methods.
23
28
 
24
29
  This Client takes control of the event loop, which you can pass in.
25
30
 
26
- One can find the function definition in the AsyncClient class.
27
31
 
28
32
  The AsyncClient is more performant and powerful, so it is recommended to use that class if possible.
29
-
30
- Avoid adding functions or other attributes to this class unless you know what you are doing, because
31
- the __getattribute__ method changes the behavior of the class in a way that is not intuitive.
32
-
33
- Instead, add them to the AsyncClient class.
33
+ Avoid adding functions or other attributes to this class unless you know what you are doing.
34
34
  """
35
35
 
36
- __slots__ = ("client", "_event_loop")
36
+ __slots__ = (
37
+ "client",
38
+ "_event_loop",
39
+ "_sync_call",
40
+ "__dict__",
41
+ )
37
42
  client: AsyncClient
38
43
  _event_loop: AbstractEventLoop
39
44
 
@@ -56,88 +61,69 @@ class Client:
56
61
  Pass in an `event_loop` if you want to use your own; otherwise, this class
57
62
  will use the default asyncio event loop.
58
63
  """
64
+ self._sync_call = self._pick_executor()
65
+
59
66
  if event_loop is None:
60
67
  try:
61
68
  event_loop = asyncio.get_running_loop()
62
69
  except RuntimeError:
63
70
  event_loop = asyncio.new_event_loop()
64
71
  asyncio.set_event_loop(event_loop)
65
- super().__setattr__("_event_loop", event_loop)
66
-
67
- async_client = self._event_loop.run_until_complete(
68
- AsyncClient.connect(
69
- api_key=api_key,
70
- api_secret=api_secret,
71
- paper_trading=paper_trading,
72
- endpoint=endpoint,
73
- graphql_port=graphql_port,
74
- **kwargs,
75
- )
72
+ object.__setattr__(self, "_event_loop", event_loop)
73
+
74
+ async_client: AsyncClient = self._sync_call(
75
+ AsyncClient.connect,
76
+ api_key=api_key,
77
+ api_secret=api_secret,
78
+ paper_trading=paper_trading,
79
+ endpoint=endpoint,
80
+ graphql_port=graphql_port,
81
+ **kwargs,
76
82
  )
77
- super().__setattr__(
83
+
84
+ object.__setattr__(
85
+ self,
78
86
  "client",
79
87
  async_client,
80
88
  )
89
+ self._promote_async_client_methods()
81
90
 
91
+ def _pick_executor(
92
+ self,
93
+ ) -> Callable[
94
+ Concatenate[Callable[P, Coroutine[Any, Any, T]], P],
95
+ T,
96
+ ]:
97
+ """Return a function that runs a coroutine and blocks."""
82
98
  if "ipykernel" in sys.modules:
83
- # for jupyter notebooks
99
+ executor = AsyncExecutor()
84
100
  import atexit
85
101
 
86
- executor = AsyncExecutor()
87
102
  atexit.register(executor.shutdown)
103
+ return lambda fn, *a, **kw: executor.submit(fn(*a, **kw))
88
104
 
89
- def _sync_call_create_task(
90
- async_method: Callable[..., Coroutine[Any, Any, T]],
91
- *args,
92
- **kwargs,
93
- ) -> T:
94
- """
95
- Executes the given coroutine synchronously using the executor.
96
- """
97
- return executor.submit(async_method(*args, **kwargs))
105
+ return lambda fn, *a, **kw: self._event_loop.run_until_complete(fn(*a, **kw))
98
106
 
99
- super().__setattr__("_sync_call", _sync_call_create_task)
107
+ def _promote_async_client_methods(self) -> None:
108
+ for name in dir(self.client):
109
+ if name.startswith("_"):
110
+ continue
100
111
 
101
- def __getattribute__(self, name: str):
102
- """
103
- You may have been lead here looking for the definition of a method of the Client
104
- It can be found if you look in the AsyncClient class, which this class is a wrapper for,
105
- or GraphQLClient, which is a parent class of AsyncClient
112
+ if any(x in name for x in ("stream", "subscribe", "connect")):
113
+ continue
114
+ attr = getattr(self.client, name)
106
115
 
107
- Explanation:
108
- __getattribute__ is a magic method that is called when searching for any attribute
109
- In this case, will look through self.client, which is an instance of the Client class
116
+ if is_async_function(attr):
117
+ attr = partial(self._sync_call, attr)
110
118
 
111
- We do this because we want to be able to call the async methods of the Client in a synchronous way,
112
- but otherwise pass through the other attributes normally
119
+ object.__setattr__(self, name, attr)
113
120
 
114
- It must be getattribute and not getattr because of the AsyncClientProtocol class inheritance
115
- We gain type hinting but lose the ability to call the methods of the Client class itself
116
- in a normal way
117
- """
118
- attr = getattr(super().__getattribute__("client"), name)
119
- if is_async_function(attr):
120
- if "subscribe" in name:
121
- raise AttributeError(
122
- f"Method {name} is an subscription based async method and cannot be called synchronously"
123
- )
124
- return partial(super().__getattribute__("_sync_call"), attr)
121
+ def __setattr__(self, name: str, value: Any) -> None:
122
+ # protect wrapper internals
123
+ if name in ("client", "_event_loop", "_sync_call"):
124
+ object.__setattr__(self, name, value)
125
125
  else:
126
- return attr
127
-
128
- def __setattr__(self, name: str, value: Any):
129
- """primarily to prevent unintended shadowing"""
130
- client = super().__getattribute__("client")
131
- setattr(client, name, value)
132
-
133
- def _sync_call(
134
- self, async_method: Callable[..., Awaitable[T]], *args, **kwargs
135
- ) -> T:
136
- return (
137
- super()
138
- .__getattribute__("_event_loop")
139
- .run_until_complete(async_method(*args, **kwargs))
140
- )
126
+ setattr(self.client, name, value)
141
127
 
142
128
 
143
129
  class AsyncExecutor:
architect_py/client.pyi CHANGED
@@ -11,7 +11,7 @@ from architect_py.graphql_client.fragments import ExecutionInfoFields as Executi
11
11
  from architect_py.grpc.client import GrpcClient as GrpcClient
12
12
  from architect_py.grpc.models.Orderflow.OrderflowRequest import OrderflowRequestUnannotatedResponseType as OrderflowRequestUnannotatedResponseType, OrderflowRequest_route as OrderflowRequest_route
13
13
  from architect_py.grpc.models.definitions import AccountIdOrName as AccountIdOrName, AccountWithPermissions as AccountWithPermissions, CandleWidth as CandleWidth, L2BookDiff as L2BookDiff, OrderId as OrderId, OrderSource as OrderSource, OrderType as OrderType, SortTickersBy as SortTickersBy, TraderIdOrEmail as TraderIdOrEmail
14
- from architect_py.grpc.resolve_endpoint import resolve_endpoint as resolve_endpoint
14
+ from architect_py.grpc.resolve_endpoint import PAPER_GRPC_PORT as PAPER_GRPC_PORT, resolve_endpoint as resolve_endpoint
15
15
  from architect_py.utils.nearest_tick import TickRoundMethod as TickRoundMethod
16
16
  from architect_py.utils.orderbook import update_orderbook_side as update_orderbook_side
17
17
  from architect_py.utils.pandas import candles_to_dataframe as candles_to_dataframe
@@ -23,19 +23,16 @@ from typing import Any, AsyncGenerator, AsyncIterator, Literal, Sequence, overlo
23
23
 
24
24
  class Client:
25
25
  """
26
+ One can find the function definition in the AsyncClient class and in the pyi file.
27
+
26
28
  This class is a wrapper around the AsyncClient class that allows you to call async methods synchronously.
27
29
  This does not work for subscription based methods.
28
30
 
29
31
  This Client takes control of the event loop, which you can pass in.
30
32
 
31
- One can find the function definition in the AsyncClient class.
32
33
 
33
34
  The AsyncClient is more performant and powerful, so it is recommended to use that class if possible.
34
-
35
- Avoid adding functions or other attributes to this class unless you know what you are doing, because
36
- the __getattribute__ method changes the behavior of the class in a way that is not intuitive.
37
-
38
- Instead, add them to the AsyncClient class.
35
+ Avoid adding functions or other attributes to this class unless you know what you are doing.
39
36
  """
40
37
  api_key: str | None
41
38
  api_secret: str | None
@@ -57,6 +54,17 @@ class Client:
57
54
  will use the default asyncio event loop.
58
55
  """
59
56
  l2_books: dict[Venue, dict[TradableProduct, tuple[L2BookSnapshot, asyncio.Task]]]
57
+ def close(self) -> None:
58
+ """
59
+ Close the gRPC channel and GraphQL client.
60
+
61
+ This fixes the:
62
+ Error in sys.excepthook:
63
+
64
+ Original exception was:
65
+
66
+ One might get when closing the client
67
+ """
60
68
  def refresh_jwt(self, force: bool = False):
61
69
  """
62
70
  Refresh the JWT for the gRPC channel if it's nearing expiration (within 1 minute).
@@ -209,15 +217,20 @@ class Client:
209
217
  Returns:
210
218
  List of futures products
211
219
  '''
212
- def get_front_future(self, series_symbol: str, venue: str, by_volume: bool = True) -> str:
220
+ def get_front_future(self, series_symbol: str, venue: str | None = None) -> TradableProduct:
213
221
  '''
214
- Gets the future with the most volume in a series.
222
+ Gets the front future.
223
+ ** If the venue is provided, it will return the future with the most volume in that venue**
224
+ Otherwise, will sort by expiration date and return the earliest future.
225
+
226
+ ** Note that this function returns a TradableProduct (ie with a base and a quote)
227
+
215
228
 
216
229
  Args:
217
230
  series_symbol: the futures series
218
231
  e.g. "ES CME Futures" would yield the lead future for the ES series
219
232
  venue: the venue to get the lead future for, e.g. "CME"
220
- by_volume: if True, sort by volume; otherwise sort by expiration date
233
+ ** If the venue is provided, it will return the future with the most volume in that venue**
221
234
 
222
235
  Returns:
223
236
  The lead future symbol
@@ -435,6 +448,35 @@ class Client:
435
448
  '''
436
449
  @deprecated(reason="Use place_limit_order instead")
437
450
  '''
451
+ def place_orders(self, order_requests: Sequence[PlaceOrderRequest]) -> list[Order]:
452
+ """
453
+ A low level function to place multiple orders in a single function.
454
+
455
+ This function does NOT check the validity of the parameters, so it is the user's responsibility
456
+ to ensure that the orders are valid and will not be rejected by the OMS.
457
+
458
+ Args:
459
+ order_request: the PlaceOrderRequest containing the orders to place
460
+
461
+
462
+ Example of a PlaceOrderRequest:
463
+ order_request: PlaceOrderRequest = PlaceOrderRequest.new(
464
+ dir=dir,
465
+ quantity=quantity,
466
+ symbol=symbol,
467
+ time_in_force=time_in_force,
468
+ limit_price=limit_price,
469
+ order_type=order_type,
470
+ account=account,
471
+ id=id,
472
+ parent_id=None,
473
+ source=OrderSource.API,
474
+ trader=trader,
475
+ execution_venue=execution_venue,
476
+ post_only=post_only,
477
+ trigger_price=trigger_price,
478
+ )
479
+ """
438
480
  def place_limit_order(self, *, id: OrderId | None = None, symbol: TradableProduct | str, execution_venue: str | None = None, dir: OrderDir | None = None, quantity: Decimal, limit_price: Decimal, order_type: OrderType = ..., time_in_force: TimeInForce = ..., price_round_method: TickRoundMethod | None = None, account: str | None = None, trader: str | None = None, post_only: bool = False, trigger_price: Decimal | None = None, **kwargs: Any) -> Order:
439
481
  '''
440
482
  Sends a regular limit order.
@@ -464,7 +506,7 @@ class Client:
464
506
 
465
507
  If the order is rejected, the order.reject_reason and order.reject_message will be set
466
508
  '''
467
- def send_market_pro_order(self, *, id: OrderId | None = None, symbol: TradableProduct | str, execution_venue: str, odir: OrderDir, quantity: Decimal, time_in_force: TimeInForce = ..., account: str | None = None, fraction_through_market: Decimal = ...) -> Order:
509
+ def send_market_pro_order(self, *, id: OrderId | None = None, symbol: TradableProduct | str, execution_venue: str, dir: OrderDir, quantity: Decimal, time_in_force: TimeInForce = ..., account: str | None = None, fraction_through_market: Decimal = ...) -> Order:
468
510
  '''
469
511
  Sends a market-order like limit price based on the BBO.
470
512
  Meant to behave as a market order but with more protections.
@@ -473,7 +515,7 @@ class Client:
473
515
  id: in case user wants to generate their own order id, otherwise it will be generated automatically
474
516
  symbol: the symbol to send the order for
475
517
  execution_venue: the execution venue to send the order to
476
- odir: the direction of the order
518
+ dir: the direction of the order
477
519
  quantity: the quantity of the order
478
520
  time_in_force: the time in force of the order
479
521
  account: the account to send the order for
@@ -65,6 +65,7 @@ class TimeInForce:
65
65
 
66
66
  @classmethod
67
67
  def GTD(cls, when: datetime) -> "TimeInForce":
68
+ assert when.tzinfo is not None, "GTD requires a timezone-aware datetime"
68
69
  return cls("GTD", when)
69
70
 
70
71
  def serialize(self) -> msgspec.Raw:
@@ -54,7 +54,7 @@ class TradableProduct(str):
54
54
  return self.split("/", 1)[1]
55
55
 
56
56
  def serialize(self) -> msgspec.Raw:
57
- return msgspec.Raw(self.encode())
57
+ return msgspec.Raw(msgspec.json.encode(str(self)))
58
58
 
59
59
  @staticmethod
60
60
  def deserialize(s: str) -> "TradableProduct":
@@ -91,6 +91,10 @@ class JuniperBaseClient:
91
91
  exc_tb: object,
92
92
  ) -> None:
93
93
  await self.http_client.aclose()
94
+
95
+ async def close(self) -> None:
96
+ """Close the HTTP client connection."""
97
+ await self.http_client.aclose()
94
98
 
95
99
  async def execute(
96
100
  self,
@@ -30,6 +30,9 @@ class GrpcClient:
30
30
  def set_jwt(self, jwt: str | None):
31
31
  self.jwt = jwt
32
32
 
33
+ async def close(self):
34
+ await self.channel.close()
35
+
33
36
  @staticmethod
34
37
  def encoder() -> msgspec.json.Encoder:
35
38
  return encoder