architect-py 3.2.2__py3-none-any.whl → 5.0.0b1__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 (169) hide show
  1. architect_py/__init__.py +8 -2
  2. architect_py/async_client.py +879 -576
  3. architect_py/client.py +25 -26
  4. architect_py/client_interface.py +62 -0
  5. architect_py/common_types/__init__.py +6 -0
  6. architect_py/common_types/order_dir.py +85 -0
  7. architect_py/common_types/scalars.py +25 -0
  8. architect_py/common_types/tradable_product.py +59 -0
  9. architect_py/graphql_client/client.py +3 -6
  10. architect_py/graphql_client/fragments.py +3 -6
  11. architect_py/graphql_client/get_fills_query.py +2 -1
  12. architect_py/graphql_client/search_symbols_query.py +2 -1
  13. architect_py/graphql_client/subscribe_orderflow.py +2 -1
  14. architect_py/graphql_client/subscribe_trades.py +2 -1
  15. architect_py/grpc/__init__.py +125 -0
  16. architect_py/grpc/client.py +86 -0
  17. architect_py/{grpc_client → grpc/models}/Accounts/AccountsRequest.py +2 -2
  18. architect_py/{grpc_client → grpc/models}/Accounts/AccountsResponse.py +1 -1
  19. architect_py/{grpc_client → grpc/models}/Accounts/__init__.py +1 -1
  20. architect_py/{grpc_client → grpc/models}/Algo/AlgoOrderForTwapAlgo.py +1 -1
  21. architect_py/{grpc_client → grpc/models}/Algo/CreateAlgoOrderRequestForTwapAlgo.py +2 -2
  22. architect_py/{grpc_client → grpc/models}/Algo/ModifyAlgoOrderRequestForTwapAlgo.py +2 -2
  23. architect_py/{grpc_client → grpc/models}/Algo/__init__.py +1 -1
  24. architect_py/grpc/models/Auth/CreateJwtRequest.py +47 -0
  25. architect_py/grpc/models/Auth/CreateJwtResponse.py +23 -0
  26. architect_py/{grpc_client/Cpty → grpc/models/Auth}/__init__.py +1 -1
  27. architect_py/grpc/models/Core/ConfigRequest.py +37 -0
  28. architect_py/grpc/models/Core/ConfigResponse.py +25 -0
  29. architect_py/{grpc_client/Folio → grpc/models/Core}/__init__.py +1 -1
  30. architect_py/{grpc_client → grpc/models}/Cpty/CptyRequest.py +2 -2
  31. architect_py/{grpc_client → grpc/models}/Cpty/CptyResponse.py +3 -3
  32. architect_py/{grpc_client → grpc/models}/Cpty/CptyStatus.py +1 -1
  33. architect_py/{grpc_client → grpc/models}/Cpty/CptyStatusRequest.py +2 -2
  34. architect_py/{grpc_client → grpc/models}/Cpty/CptysRequest.py +2 -2
  35. architect_py/{grpc_client → grpc/models}/Cpty/CptysResponse.py +1 -1
  36. architect_py/grpc/models/Cpty/__init__.py +2 -0
  37. architect_py/{grpc_client → grpc/models}/Folio/AccountHistoryRequest.py +2 -2
  38. architect_py/{grpc_client → grpc/models}/Folio/AccountHistoryResponse.py +1 -1
  39. architect_py/{grpc_client → grpc/models}/Folio/AccountSummariesRequest.py +2 -2
  40. architect_py/{grpc_client → grpc/models}/Folio/AccountSummariesResponse.py +1 -1
  41. architect_py/{grpc_client → grpc/models}/Folio/AccountSummary.py +1 -1
  42. architect_py/{grpc_client → grpc/models}/Folio/AccountSummaryRequest.py +2 -2
  43. architect_py/{grpc_client → grpc/models}/Folio/HistoricalFillsRequest.py +3 -3
  44. architect_py/{grpc_client → grpc/models}/Folio/HistoricalFillsResponse.py +1 -1
  45. architect_py/{grpc_client → grpc/models}/Folio/HistoricalOrdersRequest.py +3 -3
  46. architect_py/{grpc_client → grpc/models}/Folio/HistoricalOrdersResponse.py +1 -1
  47. architect_py/grpc/models/Folio/__init__.py +2 -0
  48. architect_py/{grpc_client → grpc/models}/Health/HealthCheckRequest.py +2 -2
  49. architect_py/{grpc_client → grpc/models}/Health/HealthCheckResponse.py +1 -1
  50. architect_py/grpc/models/Health/__init__.py +2 -0
  51. architect_py/{grpc_client → grpc/models}/Marketdata/Candle.py +1 -1
  52. architect_py/{grpc_client → grpc/models}/Marketdata/HistoricalCandlesRequest.py +11 -8
  53. architect_py/{grpc_client → grpc/models}/Marketdata/HistoricalCandlesResponse.py +1 -1
  54. architect_py/{grpc_client → grpc/models}/Marketdata/L1BookSnapshot.py +36 -3
  55. architect_py/{grpc_client → grpc/models}/Marketdata/L1BookSnapshotRequest.py +8 -3
  56. architect_py/{grpc_client → grpc/models}/Marketdata/L1BookSnapshotsRequest.py +6 -3
  57. architect_py/{grpc_client → grpc/models}/Marketdata/L2BookSnapshot.py +1 -1
  58. architect_py/{grpc_client → grpc/models}/Marketdata/L2BookSnapshotRequest.py +2 -2
  59. architect_py/{grpc_client → grpc/models}/Marketdata/Liquidation.py +2 -2
  60. architect_py/{grpc_client → grpc/models}/Marketdata/MarketStatus.py +1 -1
  61. architect_py/{grpc_client → grpc/models}/Marketdata/MarketStatusRequest.py +2 -2
  62. architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeCandlesRequest.py +2 -2
  63. architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeCurrentCandlesRequest.py +3 -4
  64. architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeL1BookSnapshotsRequest.py +6 -3
  65. architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeL2BookUpdatesRequest.py +2 -2
  66. architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeLiquidationsRequest.py +2 -2
  67. architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeManyCandlesRequest.py +2 -2
  68. architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeTickersRequest.py +2 -2
  69. architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeTradesRequest.py +2 -2
  70. architect_py/{grpc_client → grpc/models}/Marketdata/Ticker.py +1 -1
  71. architect_py/{grpc_client → grpc/models}/Marketdata/TickerRequest.py +2 -2
  72. architect_py/{grpc_client → grpc/models}/Marketdata/TickersRequest.py +2 -2
  73. architect_py/{grpc_client → grpc/models}/Marketdata/TickersResponse.py +1 -1
  74. architect_py/{grpc_client → grpc/models}/Marketdata/Trade.py +2 -2
  75. architect_py/grpc/models/Marketdata/__init__.py +2 -0
  76. architect_py/{grpc_client → grpc/models}/Oms/Cancel.py +1 -1
  77. architect_py/{grpc_client → grpc/models}/Oms/CancelAllOrdersRequest.py +2 -2
  78. architect_py/{grpc_client → grpc/models}/Oms/CancelAllOrdersResponse.py +1 -1
  79. architect_py/{grpc_client → grpc/models}/Oms/CancelOrderRequest.py +2 -2
  80. architect_py/{grpc_client → grpc/models}/Oms/OpenOrdersRequest.py +2 -2
  81. architect_py/{grpc_client → grpc/models}/Oms/OpenOrdersResponse.py +1 -1
  82. architect_py/{grpc_client → grpc/models}/Oms/Order.py +2 -2
  83. architect_py/{grpc_client → grpc/models}/Oms/PendingCancelsRequest.py +2 -2
  84. architect_py/{grpc_client → grpc/models}/Oms/PendingCancelsResponse.py +1 -1
  85. architect_py/{grpc_client → grpc/models}/Oms/PlaceOrderRequest.py +3 -3
  86. architect_py/grpc/models/Oms/__init__.py +2 -0
  87. architect_py/{grpc_client → grpc/models}/Orderflow/DropcopyRequest.py +2 -2
  88. architect_py/{grpc_client → grpc/models}/Orderflow/OrderflowRequest.py +1 -1
  89. architect_py/{grpc_client → grpc/models}/Orderflow/SubscribeOrderflowRequest.py +2 -2
  90. architect_py/grpc/models/Orderflow/__init__.py +2 -0
  91. architect_py/grpc/models/Symbology/DownloadProductCatalogRequest.py +42 -0
  92. architect_py/grpc/models/Symbology/DownloadProductCatalogResponse.py +27 -0
  93. architect_py/{grpc_client → grpc/models}/Symbology/PruneExpiredSymbolsRequest.py +2 -2
  94. architect_py/{grpc_client → grpc/models}/Symbology/PruneExpiredSymbolsResponse.py +1 -1
  95. architect_py/{grpc_client → grpc/models}/Symbology/SubscribeSymbology.py +1 -1
  96. architect_py/{grpc_client → grpc/models}/Symbology/SymbologyRequest.py +2 -2
  97. architect_py/{grpc_client → grpc/models}/Symbology/SymbologySnapshot.py +7 -2
  98. architect_py/{grpc_client → grpc/models}/Symbology/SymbologyUpdate.py +9 -2
  99. architect_py/{grpc_client → grpc/models}/Symbology/SymbolsRequest.py +2 -2
  100. architect_py/{grpc_client → grpc/models}/Symbology/SymbolsResponse.py +1 -1
  101. architect_py/grpc/models/Symbology/UploadProductCatalogRequest.py +49 -0
  102. architect_py/grpc/models/Symbology/UploadProductCatalogResponse.py +20 -0
  103. architect_py/{grpc_client → grpc/models}/Symbology/UploadSymbologyRequest.py +2 -2
  104. architect_py/{grpc_client → grpc/models}/Symbology/UploadSymbologyResponse.py +1 -1
  105. architect_py/grpc/models/Symbology/__init__.py +2 -0
  106. architect_py/grpc/models/__init__.py +2 -0
  107. architect_py/{grpc_client → grpc/models}/definitions.py +248 -66
  108. architect_py/grpc/resolve_endpoint.py +67 -0
  109. architect_py/{grpc_client/grpc_server.py → grpc/server.py} +9 -6
  110. architect_py/grpc/utils.py +32 -0
  111. architect_py/tests/conftest.py +86 -87
  112. architect_py/tests/test_book_building.py +49 -50
  113. architect_py/tests/test_marketdata.py +168 -0
  114. architect_py/tests/test_order_entry.py +37 -0
  115. architect_py/tests/test_orderflow.py +38 -0
  116. architect_py/tests/test_portfolio_management.py +23 -0
  117. architect_py/tests/test_rounding.py +28 -28
  118. architect_py/tests/test_symbology.py +37 -30
  119. architect_py/utils/nearest_tick.py +2 -5
  120. architect_py/utils/nearest_tick_2.py +1 -2
  121. architect_py/utils/orderbook.py +35 -0
  122. architect_py/utils/pandas.py +44 -0
  123. architect_py/utils/price_bands.py +0 -3
  124. architect_py/utils/symbol_parsing.py +29 -0
  125. architect_py-5.0.0b1.dist-info/METADATA +124 -0
  126. architect_py-5.0.0b1.dist-info/RECORD +184 -0
  127. {architect_py-3.2.2.dist-info → architect_py-5.0.0b1.dist-info}/WHEEL +2 -1
  128. architect_py-5.0.0b1.dist-info/top_level.txt +4 -0
  129. examples/__init__.py +0 -0
  130. examples/book_subscription.py +53 -0
  131. examples/candles.py +30 -0
  132. examples/common.py +107 -0
  133. examples/external_cpty.py +77 -0
  134. examples/funding_rate_mean_reversion_algo.py +192 -0
  135. examples/order_sending.py +92 -0
  136. examples/stream_l1_marketdata.py +25 -0
  137. examples/stream_l2_marketdata.py +40 -0
  138. examples/trades.py +21 -0
  139. examples/tutorial_async.py +84 -0
  140. examples/tutorial_sync.py +95 -0
  141. scripts/generate_functions_md.py +164 -0
  142. scripts/generate_sync_interface.py +207 -0
  143. scripts/postprocess_grpc.py +594 -0
  144. scripts/preprocess_grpc_schema.py +647 -0
  145. templates/exceptions.py +83 -0
  146. templates/juniper_base_client.py +371 -0
  147. architect_py/client_protocol.py +0 -53
  148. architect_py/grpc_client/Health/__init__.py +0 -2
  149. architect_py/grpc_client/Marketdata/__init__.py +0 -2
  150. architect_py/grpc_client/Oms/__init__.py +0 -2
  151. architect_py/grpc_client/Orderflow/__init__.py +0 -2
  152. architect_py/grpc_client/Symbology/__init__.py +0 -2
  153. architect_py/grpc_client/__init__.py +0 -2
  154. architect_py/grpc_client/grpc_client.py +0 -413
  155. architect_py/scalars.py +0 -172
  156. architect_py/tests/test_accounts.py +0 -31
  157. architect_py/tests/test_client.py +0 -29
  158. architect_py/tests/test_grpc_client.py +0 -30
  159. architect_py/tests/test_order_sending.py +0 -65
  160. architect_py/tests/test_snapshots.py +0 -52
  161. architect_py/tests/test_subscriptions.py +0 -126
  162. architect_py-3.2.2.dist-info/METADATA +0 -191
  163. architect_py-3.2.2.dist-info/RECORD +0 -148
  164. /architect_py/{grpc_client → grpc/models}/Marketdata/ArrayOfL1BookSnapshot.py +0 -0
  165. /architect_py/{grpc_client → grpc/models}/Marketdata/L2BookUpdate.py +0 -0
  166. /architect_py/{grpc_client → grpc/models}/Marketdata/TickerUpdate.py +0 -0
  167. /architect_py/{grpc_client → grpc/models}/Orderflow/Dropcopy.py +0 -0
  168. /architect_py/{grpc_client → grpc/models}/Orderflow/Orderflow.py +0 -0
  169. {architect_py-3.2.2.dist-info → architect_py-5.0.0b1.dist-info/licenses}/LICENSE +0 -0
@@ -1,216 +1,345 @@
1
- """
2
- This file composes the GraphQLClient class to provide a higher-level interface
3
- for order entry with the Architect API.
4
-
5
- These are not required to send orders, but provide typed interfaces for the
6
- various order types and algorithms that can be sent to the OMS.
7
-
8
-
9
- The functions to send orders will return the order ID string
10
- After sending the order, this string can be used to retrieve the order status
11
-
12
- send_limit_order -> get_order
13
-
14
- The individual graphql types are subject to change, so it is not recommended to use them directly.
15
- """
16
-
17
1
  import asyncio
18
- import functools
19
2
  import logging
20
- from datetime import date, datetime
3
+ import re
4
+ from datetime import date, datetime, timedelta
21
5
  from decimal import Decimal
22
- from typing import Any, AsyncIterator, List, Optional, Sequence
23
-
24
- from architect_py.graphql_client.exceptions import GraphQLClientGraphQLMultiError
25
- from architect_py.graphql_client.get_fills_query import (
26
- GetFillsQueryFolioHistoricalFills,
6
+ from typing import (
7
+ AsyncIterator,
8
+ List,
9
+ Literal,
10
+ Optional,
11
+ Sequence,
12
+ Union,
13
+ overload,
27
14
  )
28
15
 
29
- from architect_py.grpc_client.Marketdata.Candle import Candle
30
- from architect_py.grpc_client.Marketdata.HistoricalCandlesRequest import (
31
- HistoricalCandlesRequest,
32
- )
33
- from architect_py.grpc_client.Marketdata.HistoricalCandlesResponse import (
34
- HistoricalCandlesResponse,
16
+ from architect_py.grpc.models.Orderflow.Orderflow import Orderflow
17
+ from architect_py.grpc.models.Orderflow.OrderflowRequest import (
18
+ OrderflowRequest,
19
+ OrderflowRequest_route,
20
+ OrderflowRequestUnannotatedResponseType,
35
21
  )
36
- import architect_py.grpc_client.definitions as grpc_definitions
37
- from architect_py.graphql_client.place_order_mutation import PlaceOrderMutationOms
38
- from architect_py.grpc_client.Marketdata.L1BookSnapshot import L1BookSnapshot
39
- from architect_py.grpc_client.Marketdata.L2BookSnapshot import L2BookSnapshot
40
- from architect_py.grpc_client.Marketdata.L2BookUpdate import L2BookUpdate
41
- from architect_py.grpc_client.Marketdata.SubscribeCandlesRequest import (
42
- SubscribeCandlesRequest,
22
+ from architect_py.grpc.models.Orderflow.SubscribeOrderflowRequest import (
23
+ SubscribeOrderflowRequest,
43
24
  )
44
- from architect_py.grpc_client.Marketdata.SubscribeTradesRequest import (
45
- SubscribeTradesRequest,
46
- )
47
- from architect_py.grpc_client.Marketdata.Trade import Trade
48
- from architect_py.scalars import OrderDir, TradableProduct
49
- from architect_py.utils.nearest_tick import TickRoundMethod
50
25
 
26
+ from .common_types import OrderDir, TradableProduct, Venue
51
27
  from .graphql_client import GraphQLClient
52
28
  from .graphql_client.enums import (
53
29
  OrderType,
54
30
  TimeInForce,
55
31
  )
32
+ from .graphql_client.exceptions import GraphQLClientGraphQLMultiError
56
33
  from .graphql_client.fragments import (
57
34
  AccountSummaryFields,
58
35
  AccountWithPermissionsFields,
59
36
  CancelFields,
60
37
  ExecutionInfoFields,
61
- L2BookFields,
62
- MarketStatusFields,
63
- MarketTickerFields,
64
38
  OrderFields,
65
39
  ProductInfoFields,
66
40
  )
41
+ from .graphql_client.get_fills_query import GetFillsQueryFolioHistoricalFills
42
+ from .graphql_client.place_order_mutation import PlaceOrderMutationOms
43
+ from .grpc import *
44
+ from .grpc.client import GrpcClient
45
+ from .grpc.models import definitions as grpc_definitions
46
+ from .utils.nearest_tick import TickRoundMethod
47
+ from .utils.orderbook import update_orderbook_side
48
+ from .utils.price_bands import price_band_pairs
49
+ from .utils.symbol_parsing import nominative_expiration
67
50
 
68
- # from .graphql_client.input_types import (
69
- # CreateMMAlgo,
70
- # CreateOrder,
71
- # CreatePovAlgo,
72
- # CreateSmartOrderRouterAlgo,
73
- # CreateSpreadAlgo,
74
- # CreateSpreadAlgoHedgeMarket,
75
- # CreateTimeInForce,
76
- # CreateTimeInForceInstruction,
77
- # CreateTwapAlgo,
78
- # )
79
- from .grpc_client import GRPCClient
51
+ try:
52
+ import pandas as pd
80
53
 
81
- from .utils.price_bands import price_band_pairs
54
+ from .utils.pandas import candles_to_dataframe
82
55
 
83
- logger = logging.getLogger(__name__)
56
+ FEATURE_PANDAS = True
57
+ except ImportError:
58
+ FEATURE_PANDAS = False
84
59
 
85
60
 
86
61
  class AsyncClient:
62
+ api_key: Optional[str] = None
63
+ api_secret: Optional[str] = None
64
+ paper_trading: bool
87
65
  graphql_client: GraphQLClient
88
- grpc_client: GRPCClient
66
+ grpc_core: Optional[GrpcClient] = None
67
+ grpc_marketdata: dict[Venue, GrpcClient] = {}
68
+ grpc_hmart: Optional[GrpcClient] = None
69
+ jwt: str | None = None
70
+ jwt_expiration: datetime | None = None
71
+
72
+ l1_books: dict[Venue, dict[TradableProduct, tuple[L1BookSnapshot, asyncio.Task]]]
73
+ l2_books: dict[Venue, dict[TradableProduct, tuple[L2BookSnapshot, asyncio.Task]]]
89
74
 
90
75
  # ------------------------------------------------------------
91
- # Initialization
76
+ # Initialization and configuration
92
77
  # ------------------------------------------------------------
93
78
 
94
79
  @staticmethod
95
80
  async def connect(
96
81
  *,
97
- api_key: str,
98
- api_secret: str,
82
+ api_key: Optional[str] = None,
83
+ api_secret: Optional[str] = None,
99
84
  paper_trading: bool,
100
- host: str = "app.architect.co",
101
- grpc_endpoint: str = "cme.marketdata.architect.co",
102
- _port: Optional[int] = None,
103
- **kwargs: Any,
85
+ endpoint: str = "https://app.architect.co",
86
+ graphql_port: Optional[int] = None,
104
87
  ) -> "AsyncClient":
105
88
  """
106
- The main way to create an AsyncClient object.
107
-
108
- Args:
109
- api_key: API key for the user
110
- api_secret: API secret for the user
111
- host: Host for the GraphQL server, defaults to "app.architect.co"
112
- paper_trading: Whether to use the paper trading environment, defaults to True
113
- _port: Port for the GraphQL server, more for debugging purposes, do not set this unless you are sure of the port
114
-
115
- the API key and secret can be generated on the app.architect.co website
89
+ Connect to an Architect installation.
116
90
 
117
- Returns:
118
- Client object
119
-
120
- Raises:
121
- ValueError: If the API key or secret are not the correct length or contain invalid characters
122
-
123
- For any request, if you get a "GraphQLClientHttpError: HTTP status code: 500" it likely means that your
124
- API key and secret are incorrect. Please double check your credentials.
125
-
126
- If you get a "GraphQLClientHttpError: HTTP status code: 400", please contact support so we can fix the function.
127
-
128
- If you get an AttributeError on the grpc_client, it means that the GRPC client has not been initialized
129
- likely due to the client not being instantiated with the connect method
91
+ Raises ValueError if the API key and secret are not the correct length or contain invalid characters.
130
92
  """
131
93
  if paper_trading:
132
- logger.critical(
133
- "You are using the paper trading environment. Please make sure to switch to the live environment when you are ready."
134
- )
94
+ COLOR = "\033[30;43m"
95
+ RESET = "\033[0m"
96
+ print(f"🧻 {COLOR} YOU ARE IN PAPER TRADING MODE {RESET}")
97
+
98
+ grpc_host, grpc_port, use_ssl = await resolve_endpoint(endpoint)
99
+ logging.info(
100
+ f"Resolved endpoint {endpoint}: {grpc_host}:{grpc_port} use_ssl={use_ssl}"
101
+ )
135
102
 
136
- async_client = AsyncClient(
103
+ client = AsyncClient(
137
104
  api_key=api_key,
138
105
  api_secret=api_secret,
139
- host=host,
140
106
  paper_trading=paper_trading,
141
- _port=_port,
107
+ grpc_host=grpc_host,
108
+ grpc_port=grpc_port,
109
+ graphql_port=graphql_port,
110
+ use_ssl=use_ssl,
142
111
  _i_know_what_i_am_doing=True,
143
- **kwargs,
144
112
  )
145
113
 
146
- async_client.grpc_client = GRPCClient(
147
- async_client.graphql_client, grpc_endpoint
148
- )
149
- await async_client.grpc_client.initialize()
150
- return async_client
114
+ logging.info("Exchanging credentials...")
115
+ await client.refresh_jwt()
116
+
117
+ logging.info("Discovering marketdata endpoints...")
118
+ await client.discover_marketdata()
119
+
120
+ return client
151
121
 
152
122
  def __init__(
153
123
  self,
154
124
  *,
155
- api_key: str,
156
- api_secret: str,
157
- host: str = "app.architect.co",
158
- paper_trading: bool = True,
159
- _port: Optional[int] = None,
125
+ api_key: Optional[str] = None,
126
+ api_secret: Optional[str] = None,
127
+ paper_trading: bool,
128
+ grpc_host: str = "app.architect.co",
129
+ grpc_port: int,
130
+ graphql_port: Optional[int] = None,
131
+ use_ssl: bool = True,
160
132
  _i_know_what_i_am_doing: bool = False,
161
- **kwargs: Any,
162
133
  ):
163
134
  """
164
- Users should essentially never be using this constructor directly.
165
-
166
- Use the connect method instead.
167
- See self.connect for arg explanations
135
+ Use AsyncClient.connect instead.
168
136
  """
169
-
170
137
  if not _i_know_what_i_am_doing:
171
- raise ValueError(
172
- "Please use the connect method to create an AsyncClient object."
173
- )
138
+ raise ValueError("Use AsyncClient.connect to create an AsyncClient object.")
174
139
 
175
- if not api_key.isalnum():
176
- raise ValueError(
177
- "API key must be alphanumeric, please double check your credentials."
178
- )
179
- elif "," in api_key or "," in api_secret:
180
- raise ValueError(
181
- "API key and secret cannot contain commas, please double check your credentials."
182
- )
183
- elif " " in api_key or " " in api_secret:
140
+ if api_key is not None and not re.match(r"^[a-zA-Z0-9]{24}$", api_key):
141
+ raise ValueError("API key must be exactly 24 alphanumeric characters")
142
+ if api_secret is not None and not re.match(
143
+ r"^[a-zA-Z0-9+\/=]{44}$", api_secret
144
+ ):
184
145
  raise ValueError(
185
- "API key and secret cannot contain spaces, please double check your credentials."
146
+ "API secret must be a Base64-encoded string, 44 characters long"
186
147
  )
187
- elif len(api_key) != 24 or len(api_secret) != 44:
148
+
149
+ if paper_trading and (graphql_port is not None or grpc_port is not None):
188
150
  raise ValueError(
189
- "API key and secret are not the correct length, please double check your credentials."
151
+ "If paper_trading is True, graphql_port and grpc_port must be None"
190
152
  )
191
153
 
192
- if _port is None:
154
+ if graphql_port is None:
193
155
  if paper_trading:
194
- _port = 5678
156
+ graphql_port = 5678
195
157
  else:
196
- _port = 4567
158
+ graphql_port = 4567
197
159
 
160
+ self.api_key = api_key
161
+ self.api_secret = api_secret
162
+ self.paper_trading = paper_trading
198
163
  self.graphql_client = GraphQLClient(
199
- api_key=api_key, api_secret=api_secret, host=host, port=_port, **kwargs
164
+ host=grpc_host,
165
+ port=graphql_port,
166
+ use_tls=use_ssl,
167
+ api_key=api_key,
168
+ api_secret=api_secret,
200
169
  )
170
+ self.grpc_core = GrpcClient(host=grpc_host, port=grpc_port, use_ssl=use_ssl)
171
+
172
+ async def refresh_jwt(self, force: bool = False):
173
+ """
174
+ Refresh the JWT for the gRPC channel if it's nearing expiration (within 1 minute).
175
+ If force=True, refresh the JWT unconditionally.
176
+
177
+ Query methods on AsyncClient that require auth will call this method internally.
178
+ """
179
+ if not self.api_key or not self.api_secret:
180
+ raise ValueError("API key and secret not set")
181
+ if self.grpc_core is None:
182
+ raise ValueError("gRPC client to Architect not initialized")
183
+
184
+ if (
185
+ force
186
+ or self.jwt_expiration is None
187
+ or datetime.now() > self.jwt_expiration - timedelta(minutes=1)
188
+ ):
189
+ try:
190
+ req = CreateJwtRequest(api_key=self.api_key, api_secret=self.api_secret)
191
+ res: CreateJwtResponse = await self.grpc_core.unary_unary(req)
192
+ self.jwt = res.jwt
193
+ # CR alee: actually read the JWT to get the expiration time;
194
+ # for now, we just "know" that the JWTs are granted for an hour
195
+ self.jwt_expiration = datetime.now() + timedelta(hours=1)
196
+ except Exception as e:
197
+ logging.error("Failed to refresh gRPC credentials: %s", e)
198
+
199
+ def set_jwt(self, jwt: str | None, jwt_expiration: datetime | None = None):
200
+ """
201
+ Manually set the JWT for gRPC authentication.
202
+
203
+ Args:
204
+ jwt: the JWT to set;
205
+ None to clear the JWT
206
+ jwt_expiration: when to expire the JWT
207
+ """
208
+ self.jwt = jwt
209
+ self.jwt_expiration = jwt_expiration
201
210
 
202
- async def enable_orderflow(self):
211
+ async def discover_marketdata(self):
203
212
  """
204
- Load and cache product and execution info so that the SDK can send orders.
213
+ Load marketdata endpoints from the server config.
205
214
 
206
- CR alee: determine if this is better than @functools.lru_cache
215
+ The Architect core is responsible for telling you where to find marketdata as per
216
+ its configuration. You can also manually set marketdata endpoints by calling
217
+ set_marketdata directly.
218
+
219
+ This method is called on AsyncClient.connect.
207
220
  """
208
- pass
221
+ try:
222
+ grpc_client = await self.core()
223
+ req = ConfigRequest()
224
+ res: ConfigResponse = await grpc_client.unary_unary(req)
225
+ for venue, endpoint in res.marketdata.items():
226
+ try:
227
+ grpc_host, grpc_port, use_ssl = await resolve_endpoint(endpoint)
228
+ logging.info(
229
+ "Setting marketdata endpoint for %s: %s:%d use_ssl=%s",
230
+ venue,
231
+ grpc_host,
232
+ grpc_port,
233
+ use_ssl,
234
+ )
235
+ self.grpc_marketdata[venue] = GrpcClient(
236
+ host=grpc_host, port=grpc_port, use_ssl=use_ssl
237
+ )
238
+ except Exception as e:
239
+ logging.error("Failed to set marketdata endpoint: %s", e)
240
+ except Exception as e:
241
+ logging.error("Failed to get marketdata config: %s", e)
242
+
243
+ async def set_marketdata(self, venue: Venue, endpoint: str):
244
+ """
245
+ Manually set the marketdata endpoint for a venue.
246
+ """
247
+ try:
248
+ grpc_host, grpc_port, use_ssl = await resolve_endpoint(endpoint)
249
+ self.grpc_marketdata[venue] = GrpcClient(
250
+ host=grpc_host, port=grpc_port, use_ssl=use_ssl
251
+ )
252
+ except Exception as e:
253
+ logging.error("Failed to set marketdata endpoint: %s", e)
254
+
255
+ async def marketdata(self, venue: Venue) -> GrpcClient:
256
+ """
257
+ Get the marketdata client for a venue.
258
+ """
259
+ if venue not in self.grpc_marketdata:
260
+ raise ValueError(f"Marketdata not configured for venue: {venue}")
261
+
262
+ await self.refresh_jwt()
263
+ self.grpc_marketdata[venue].set_jwt(self.jwt)
264
+ return self.grpc_marketdata[venue]
265
+
266
+ async def set_hmart(self, endpoint: str):
267
+ """
268
+ Manually set the hmart (historical marketdata service) endpoint.
269
+ """
270
+ try:
271
+ grpc_host, grpc_port, use_ssl = await resolve_endpoint(endpoint)
272
+ logging.info(
273
+ "Resolved hmart endpoint %s: %s:%d use_ssl=%s",
274
+ endpoint,
275
+ grpc_host,
276
+ grpc_port,
277
+ use_ssl,
278
+ )
279
+ self.grpc_hmart = GrpcClient(
280
+ host=grpc_host, port=grpc_port, use_ssl=use_ssl
281
+ )
282
+ except Exception as e:
283
+ logging.error("Failed to set hmart endpoint: %s", e)
284
+
285
+ async def hmart(self) -> GrpcClient:
286
+ """
287
+ Get the hmart (historical marketdata service) client.
288
+ """
289
+ if self.grpc_hmart is None:
290
+ # default to historical.marketdata.architect.co
291
+ await self.set_hmart("https://historical.marketdata.architect.co")
292
+
293
+ if self.grpc_hmart is None:
294
+ raise ValueError("hmart client not initialized")
295
+
296
+ await self.refresh_jwt()
297
+ self.grpc_hmart.set_jwt(self.jwt)
298
+ return self.grpc_hmart
299
+
300
+ async def core(self) -> GrpcClient:
301
+ """
302
+ Get the core client.
303
+ """
304
+ if self.grpc_core is None:
305
+ raise ValueError("gRPC client to Architect not initialized")
306
+
307
+ await self.refresh_jwt()
308
+ self.grpc_core.set_jwt(self.jwt)
309
+ return self.grpc_core
310
+
311
+ async def who_am_i(self) -> tuple[str, str]:
312
+ """
313
+ Gets the user_id and user_email for the user that the API key belongs to.
314
+
315
+ Returns:
316
+ (user_id, user_email)
317
+ """
318
+ res = await self.graphql_client.user_id_query()
319
+ return res.user_id, res.user_email
209
320
 
210
321
  # ------------------------------------------------------------
211
322
  # Symbology
212
323
  # ------------------------------------------------------------
213
324
 
325
+ async def list_symbols(self, *, marketdata: Optional[Venue] = None) -> list[str]:
326
+ """
327
+ List all symbols.
328
+
329
+ Args:
330
+ marketdata: query marketdata endpoint for the specified venue directly;
331
+ If provided, query the venue's marketdata endpoint directly,
332
+ instead of the Architect core. This is sometimes useful for
333
+ cross-referencing symbols or checking availability.
334
+ """
335
+ if marketdata is not None:
336
+ grpc_client = await self.marketdata(marketdata)
337
+ else:
338
+ grpc_client = await self.core()
339
+ req = SymbolsRequest()
340
+ res: SymbolsResponse = await grpc_client.unary_unary(req)
341
+ return res.symbols
342
+
214
343
  async def search_symbols(
215
344
  self,
216
345
  search_string: Optional[str] = None,
@@ -219,75 +348,70 @@ class AsyncClient:
219
348
  limit: int = 20,
220
349
  ) -> List[TradableProduct]:
221
350
  """
222
- Search for symbols in the Architect database.
351
+ Search for tradable products on Architect.
223
352
 
224
353
  Args:
225
- search_string: a string to search for in the symbol. Can be "*" for wild card search.
354
+ search_string: a string to search for in the symbol
355
+ Can be "*" for wild card search.
226
356
  Examples: "ES", "NQ", "GC"
227
357
  execution_venue: the execution venue to search in
228
358
  Examples: "CME"
229
- Returns:
230
- a list of TradableProduct objects
231
- """
232
- markets = (
233
- await self.graphql_client.search_symbols_query(
234
- search_string=search_string,
235
- execution_venue=execution_venue,
236
- offset=offset,
237
- limit=limit,
238
- )
239
- ).search_symbols
240
-
241
- return markets
359
+ """
360
+ res = await self.graphql_client.search_symbols_query(
361
+ search_string=search_string,
362
+ execution_venue=execution_venue,
363
+ offset=offset,
364
+ limit=limit,
365
+ )
366
+ return res.search_symbols
242
367
 
243
368
  async def get_product_info(self, symbol: str) -> Optional[ProductInfoFields]:
244
369
  """
245
- Get the product information (product_type, underlying, multiplier, etc.) for a symbol.
370
+ Get information about a product, e.g. product_type, underlying, multiplier.
246
371
 
247
372
  Args:
248
373
  symbol: the symbol to get information for
374
+
249
375
  Returns:
250
- ProductInfoFields object if the symbol exists
376
+ None if the symbol does not exist
251
377
  """
252
- info = await self.graphql_client.get_product_info_query(symbol)
253
- return info.product_info
378
+ res = await self.graphql_client.get_product_info_query(symbol)
379
+ return res.product_info
254
380
 
255
381
  async def get_product_infos(
256
382
  self, symbols: Optional[list[str]]
257
383
  ) -> Sequence[ProductInfoFields]:
258
384
  """
259
- Get the product information (product_type, underlying, multiplier, etc.) for a list of symbols.
385
+ Get information about products, e.g. product_type, underlying, multiplier.
260
386
 
261
387
  Args:
262
- symbols: the symbols to get information for
263
- Returns:
264
- a list of ProductInfoFields
265
-
266
- Any duplicate or invalid symbols will be ignored.
267
- The order of the symbols in the list will not necessarily be preserved in the output.
388
+ symbols: the symbols to get information for, or None for all symbols
268
389
 
269
- If you want the entire universe of symbols, pass in None
390
+ Returns:
391
+ Product infos for each symbol. Not guaranteed to contain all symbols
392
+ that were asked for, or in the same order; any duplicates or invalid
393
+ symbols will be ignored.
270
394
  """
271
- infos = await self.graphql_client.get_product_infos_query(symbols)
272
- return infos.product_infos
395
+ res = await self.graphql_client.get_product_infos_query(symbols)
396
+ return res.product_infos
273
397
 
274
398
  async def get_execution_info(
275
- self, symbol: TradableProduct, execution_venue: str
399
+ self, symbol: TradableProduct | str, execution_venue: str
276
400
  ) -> Optional[ExecutionInfoFields]:
277
401
  """
278
- Get the execution information (tick_size, step_size, margin, etc.) for a symbol.
402
+ Get information about tradable product execution, e.g. tick_size,
403
+ step_size, margins.
279
404
 
280
405
  Args:
281
406
  symbol: the symbol to get execution information for
282
407
  execution_venue: the execution venue e.g. "CME"
283
408
 
284
409
  Returns:
285
- ExecutionInfoFields
286
-
410
+ None if the symbol doesn't exist
287
411
  """
288
412
  try:
289
413
  execution_info = await self.graphql_client.get_execution_info_query(
290
- symbol, execution_venue
414
+ TradableProduct(symbol), execution_venue
291
415
  )
292
416
  return execution_info.execution_info
293
417
  except GraphQLClientGraphQLMultiError:
@@ -301,59 +425,60 @@ class AsyncClient:
301
425
  execution_venue: Optional[str] = None,
302
426
  ) -> Sequence[ExecutionInfoFields]:
303
427
  """
304
- Get the execution information (tick_size, step_size, etc.) for a list of symbols.
428
+ Get information about tradable product execution, e.g. tick_size,
429
+ step_size, margins, for many symbols.
305
430
 
306
431
  Args:
307
- symbols: the symbols to get execution information for
432
+ symbols: the symbols to get execution information for, or None for all symbols
308
433
  execution_venue: the execution venue e.g. "CME"
309
434
 
310
435
  Returns:
311
- a list of ExecutionInfoFields
312
-
313
- If you want the entire universe of execution infos, pass in None
436
+ Execution infos for each symbol. Not guaranteed to contain all symbols
437
+ that were asked for, or in the same order; any duplicates or invalid
438
+ symbols will be ignored.
314
439
  """
315
- execution_infos = await self.graphql_client.get_execution_infos_query(
440
+ res = await self.graphql_client.get_execution_infos_query(
316
441
  symbols, execution_venue
317
442
  )
318
- return execution_infos.execution_infos
443
+ return res.execution_infos
319
444
 
320
445
  async def get_cme_first_notice_date(self, symbol: str) -> Optional[date]:
321
446
  """
447
+ @deprecated(reason="Use get_product_info instead; first_notice_date is now a field")
448
+
322
449
  Get the first notice date for a CME future.
323
450
 
324
451
  Args:
325
452
  symbol: the symbol to get the first notice date for a CME future
326
453
 
327
454
  Returns:
328
- the first notice date as a date object if it exists
455
+ The first notice date as a date object if it exists
329
456
  """
330
- notice = await self.graphql_client.get_first_notice_date_query(symbol)
331
- if notice is None or notice.product_info is None:
457
+ res = await self.graphql_client.get_first_notice_date_query(symbol)
458
+ if res is None or res.product_info is None:
332
459
  return None
333
- return notice.product_info.first_notice_date
460
+ return res.product_info.first_notice_date
334
461
 
335
- async def get_future_series(self, series_symbol: str) -> list[str]:
462
+ async def get_futures_series(self, series_symbol: str) -> list[str]:
336
463
  """
337
- Get the series of futures for a given series symbol.
464
+ List all futures in a given series.
338
465
 
339
466
  Args:
340
- series_symbol: the symbol to get the series for
341
- e.g. ES CME Futures" would yield a list of all the ES futures
467
+ series_symbol: the futures series
468
+ e.g. "ES CME Futures" would yield a list of all the ES futures
342
469
  Returns:
343
- a list of symbols in the series
470
+ List of futures products
344
471
  """
345
- assert (
346
- " " in series_symbol
347
- ), 'series_symbol must have the venue in it, e.g. "ES CME Futures" or "GC CME Futures"'
348
-
349
- futures_series = await self.graphql_client.get_future_series_query(
350
- series_symbol
351
- )
352
- return futures_series.futures_series
472
+ if not series_symbol.endswith("Futures"):
473
+ raise ValueError("series_symbol must end with 'Futures'")
474
+ res = await self.graphql_client.get_future_series_query(series_symbol)
475
+ return res.futures_series
353
476
 
354
477
  @staticmethod
355
- def get_expiration_from_CME_name(name: str) -> date:
478
+ def get_expiration_from_CME_name(name: str) -> Optional[date]:
356
479
  """
480
+ @deprecated(reason="Use utils.symbol_parsing.nominative_expiration instead")
481
+
357
482
  Get the expiration date from a CME future name.
358
483
 
359
484
  Args:
@@ -362,12 +487,13 @@ class AsyncClient:
362
487
  Returns:
363
488
  the expiration date as a date object
364
489
  """
365
- _, d, *_ = name.split(" ")
366
- return datetime.strptime(d, "%Y%m%d").date()
490
+ return nominative_expiration(name)
367
491
 
368
492
  async def get_cme_futures_series(self, series: str) -> list[tuple[date, str]]:
369
493
  """
370
- Get the futures in a series from the CME.
494
+ @deprecated(reason="Use get_futures_series instead")
495
+
496
+ List all futures in a given CME series.
371
497
 
372
498
  Args:
373
499
  series: the series to get the futures for
@@ -383,33 +509,28 @@ class AsyncClient:
383
509
  ...
384
510
  ]
385
511
  """
386
- markets = await self.get_future_series(
387
- series,
388
- )
389
-
390
- filtered_markets = [
391
- (self.get_expiration_from_CME_name(market), market) for market in markets
392
- ]
393
-
394
- filtered_markets.sort(key=lambda x: x[0])
395
-
396
- return filtered_markets
512
+ futures = await self.get_futures_series(series)
513
+ exp_and_futures = []
514
+ for future in futures:
515
+ exp = nominative_expiration(future)
516
+ if exp is not None:
517
+ exp_and_futures.append((exp, future))
518
+ exp_and_futures.sort(key=lambda x: x[0])
519
+ return exp_and_futures
397
520
 
398
521
  async def get_cme_future_from_root_month_year(
399
522
  self, root: str, month: int, year: int
400
523
  ) -> str:
401
524
  """
402
525
  Get the symbol for a CME future from the root, month, and year.
526
+ This is a simple wrapper around search_symbols.
403
527
 
404
528
  Args:
405
529
  root: the root symbol for the future e.g. "ES"
406
530
  month: the month of the future
407
531
  year: the year of the future
408
532
  Returns:
409
- the symbol for the future
410
-
411
- Errors if the result is not unique
412
- This is a simple wrapper around search_symbols
533
+ The future symbol if it exists and is unique.
413
534
  """
414
535
  [market] = [
415
536
  market
@@ -419,23 +540,393 @@ class AsyncClient:
419
540
  )
420
541
  if market.startswith(f"{root} {year}{month:02d}")
421
542
  ]
422
-
423
543
  return market
424
544
 
425
545
  # ------------------------------------------------------------
426
- # Account Management
546
+ # Marketdata
427
547
  # ------------------------------------------------------------
428
548
 
429
- async def who_am_i(self) -> tuple[str, str]:
549
+ async def get_market_status(
550
+ self, symbol: TradableProduct | str, venue: Venue
551
+ ) -> MarketStatus:
430
552
  """
431
- Gets the user_id and user_email for the user that the API key belongs to.
553
+ Returns market status for symbol (e.g. if it's currently quoting or trading).
554
+
555
+ Args:
556
+ symbol: the symbol to get the market status for, e.g. "ES 20250321 CME Future/USD"
557
+ venue: the venue that the symbol is traded at, e.g. CME
558
+ """
559
+ grpc_client = await self.marketdata(venue)
560
+ req = MarketStatusRequest(symbol=str(symbol), venue=venue)
561
+ res: MarketStatus = await grpc_client.unary_unary(req)
562
+ return res
432
563
 
564
+ async def get_market_snapshot(
565
+ self, symbol: TradableProduct | str, venue: Venue
566
+ ) -> L1BookSnapshot:
567
+ """
568
+ @deprecated(reason="Use get_l1_snapshot instead")
569
+
570
+ This is an alias for l1_book_snapshot.
571
+
572
+ Args:
573
+ symbol: the symbol to get the market snapshot for, e.g. "ES 20250321 CME Future/USD"
574
+ venue: the venue that the symbol is traded at, e.g. CME
433
575
  Returns:
434
- (user_id, user_email)
576
+ MarketTickerFields for the symbol
577
+ """
578
+ return await self.get_l1_book_snapshot(symbol=symbol, venue=venue)
579
+
580
+ async def get_market_snapshots(
581
+ self, symbols: list[TradableProduct | str], venue: Venue
582
+ ) -> Sequence[L1BookSnapshot]:
583
+ """
584
+ @deprecated(reason="Use get_l1_snapshots instead")
585
+
586
+ This is an alias for l1_book_snapshots.
587
+
588
+ Args:
589
+ symbols: the symbols to get the market snapshots for
590
+ venue: the venue that the symbols are traded at
591
+ """
592
+ return await self.get_l1_book_snapshots(
593
+ venue=venue,
594
+ symbols=symbols, # type: ignore
595
+ )
596
+
597
+ @overload
598
+ async def get_historical_candles(
599
+ self,
600
+ symbol: TradableProduct | str,
601
+ venue: Venue,
602
+ candle_width: CandleWidth,
603
+ start: datetime,
604
+ end: datetime,
605
+ *,
606
+ as_dataframe: Literal[True],
607
+ ) -> pd.DataFrame: ...
608
+
609
+ @overload
610
+ async def get_historical_candles(
611
+ self,
612
+ symbol: TradableProduct | str,
613
+ venue: Venue,
614
+ candle_width: CandleWidth,
615
+ start: datetime,
616
+ end: datetime,
617
+ ) -> List[Candle]: ...
618
+
619
+ async def get_historical_candles(
620
+ self,
621
+ symbol: TradableProduct | str,
622
+ venue: Venue,
623
+ candle_width: CandleWidth,
624
+ start: datetime,
625
+ end: datetime,
626
+ *,
627
+ as_dataframe: bool = False,
628
+ ) -> Union[List[Candle], pd.DataFrame]:
629
+ """
630
+ Gets historical candles for a symbol.
631
+
632
+ Args:
633
+ symbol: the symbol to get the candles for
634
+ venue: the venue of the symbol
635
+ candle_width: the width of the candles
636
+ start: the start date to get candles for;
637
+ For naive datetimes, the server will assume UTC.
638
+ end: the end date to get candles for;
639
+ For naive datetimes, the server will assume UTC.
640
+ as_dataframe: if True, return a pandas DataFrame
641
+
642
+ """
643
+ grpc_client = await self.hmart()
644
+ req = HistoricalCandlesRequest(
645
+ venue=venue,
646
+ symbol=str(symbol),
647
+ candle_width=candle_width,
648
+ start_date=start,
649
+ end_date=end,
650
+ )
651
+ res: HistoricalCandlesResponse = await grpc_client.unary_unary(req)
652
+
653
+ if as_dataframe and FEATURE_PANDAS:
654
+ return candles_to_dataframe(res.candles)
655
+ elif as_dataframe and not FEATURE_PANDAS:
656
+ raise RuntimeError("as_dataframe is True but pandas is not installed")
657
+ else:
658
+ return res.candles
659
+
660
+ async def get_l1_book_snapshot(
661
+ self,
662
+ symbol: TradableProduct | str,
663
+ venue: Venue,
664
+ ) -> L1BookSnapshot:
665
+ """
666
+ Gets the L1 book snapshot for a symbol.
667
+
668
+ Args:
669
+ symbol: the symbol to get the l1 book snapshot for
670
+ venue: the venue that the symbol is traded at
671
+ """
672
+ grpc_client = await self.marketdata(venue)
673
+ req = L1BookSnapshotRequest(symbol=str(symbol), venue=venue)
674
+ res: L1BookSnapshot = await grpc_client.unary_unary(req)
675
+ return res
676
+
677
+ async def get_l1_book_snapshots(
678
+ self, symbols: list[TradableProduct | str], venue: Venue
679
+ ) -> Sequence[L1BookSnapshot]:
680
+ """
681
+ Gets the L1 book snapshots for a list of symbols.
682
+
683
+ Args:
684
+ symbols: the symbols to get the l1 book snapshots for
685
+ venue: the venue that the symbols are traded at
686
+ """
687
+ grpc_client = await self.marketdata(venue)
688
+ req = L1BookSnapshotsRequest(symbols=symbols)
689
+ res: ArrayOfL1BookSnapshot = await grpc_client.unary_unary(req) # type: ignore
690
+ return res
691
+
692
+ async def get_l2_book_snapshot(
693
+ self, symbol: TradableProduct | str, venue: Venue
694
+ ) -> L2BookSnapshot:
695
+ """
696
+ Gets the L2 book snapshot for a symbol.
697
+
698
+ Args:
699
+ symbol: the symbol to get the l2 book snapshot for
700
+ venue: the venue that the symbol is traded at
701
+ """
702
+ grpc_client = await self.marketdata(venue)
703
+ req = L2BookSnapshotRequest(symbol=str(symbol), venue=venue)
704
+ res: L2BookSnapshot = await grpc_client.unary_unary(req)
705
+ return res
706
+
707
+ async def get_ticker(self, symbol: TradableProduct | str, venue: Venue) -> Ticker:
708
+ """
709
+ Gets the ticker for a symbol.
710
+ """
711
+ grpc_client = await self.marketdata(venue)
712
+ req = TickerRequest(symbol=str(symbol), venue=venue)
713
+ res: Ticker = await grpc_client.unary_unary(req)
714
+ return res
715
+
716
+ async def stream_l1_book_snapshots(
717
+ self, symbols: Sequence[TradableProduct | str], venue: Venue
718
+ ) -> AsyncIterator[L1BookSnapshot]:
719
+ """
720
+ Subscribe to the stream of L1BookSnapshots for a symbol.
721
+
722
+ Args:
723
+ symbols: the symbols to subscribe to;
724
+ If symbols=None, subscribe to all symbols available for the venue.
725
+ venue: the venue to subscribe to
726
+ """
727
+ grpc_client = await self.marketdata(venue)
728
+ req = SubscribeL1BookSnapshotsRequest(symbols=list(symbols), venue=venue)
729
+ return grpc_client.unary_stream(req)
730
+
731
+ async def stream_l2_book_updates(
732
+ self, symbol: TradableProduct | str, venue: Venue
733
+ ) -> AsyncIterator[L2BookUpdate]:
734
+ """
735
+ Subscribe to the stream of L2BookUpdates for a symbol.
736
+
737
+ This stream is a diff stream; to construct and maintain the actual state of
738
+ the L2 book, apply the updates stream using the method described.
739
+
740
+ Args:
741
+ symbol: the symbol to subscribe to
742
+ venue: the marketdata venue
743
+ """
744
+ grpc_client = await self.marketdata(venue)
745
+ req = SubscribeL2BookUpdatesRequest(symbol=str(symbol), venue=venue)
746
+ return grpc_client.unary_stream(req) # type: ignore
747
+
748
+ async def subscribe_l1_book(
749
+ self, symbol: TradableProduct | str, venue: Venue
750
+ ) -> L1BookSnapshot:
751
+ """
752
+ Subscribe to the L1 stream for a symbol in the background.
753
+
754
+ If a subscription is already active, the existing reference will be
755
+ returned; otherwise, a new subscription will be created.
756
+
757
+ Snapshots will have an initial value of timestamp=0 and bid/ask=None.
758
+
759
+ Args:
760
+ symbol: the symbol to subscribe to
761
+ venue: the marketdata venue
762
+
763
+ Return:
764
+ An L1 book object that is constantly updating in the background.
765
+ """
766
+ symbol = TradableProduct(symbol)
767
+
768
+ if venue in self.l1_books:
769
+ if symbol in self.l1_books[venue]:
770
+ return self.l1_books[venue][symbol][0]
771
+ else:
772
+ self.l1_books[venue] = {}
773
+
774
+ grpc_client = await self.marketdata(venue)
775
+ book = L1BookSnapshot(symbol, 0, 0)
776
+ self.l1_books[venue][symbol] = (
777
+ book,
778
+ asyncio.create_task(
779
+ self.__subscribe_l1_book_task(symbol, venue, grpc_client, book)
780
+ ),
781
+ )
782
+ return book
783
+
784
+ async def unsubscribe_l1_book(self, symbol: TradableProduct | str, venue: Venue):
785
+ symbol = TradableProduct(symbol)
786
+ try:
787
+ task = self.l1_books[venue][symbol][1]
788
+ task.cancel()
789
+ except Exception as e:
790
+ logging.error(
791
+ f"Error unsubscribing from L1 book for {symbol}, venue {venue}: {e}"
792
+ )
793
+ finally:
794
+ if venue in self.l1_books and symbol in self.l1_books[venue]:
795
+ del self.l1_books[venue][symbol]
796
+
797
+ async def __subscribe_l1_book_task(
798
+ self,
799
+ symbol: TradableProduct,
800
+ venue: Venue,
801
+ grpc_client: GrpcClient,
802
+ book: L1BookSnapshot,
803
+ ):
804
+ try:
805
+ req = SubscribeL1BookSnapshotsRequest(symbols=[symbol], venue=venue)
806
+ stream = grpc_client.unary_stream(req)
807
+ async for snap in stream:
808
+ book.tn = snap.tn
809
+ book.ts = snap.ts
810
+ book.a = snap.a
811
+ book.b = snap.b
812
+ book.rt = snap.rt
813
+ book.rtn = snap.rtn
814
+ except Exception as e:
815
+ logging.error(
816
+ f"Error subscribing to L1 book for {symbol}, venue {venue}: {e}"
817
+ )
818
+ finally:
819
+ del self.l1_books[venue][symbol]
820
+
821
+ async def subscribe_l2_book(
822
+ self,
823
+ symbol: TradableProduct | str,
824
+ venue: Venue,
825
+ ) -> L2BookSnapshot:
826
+ """
827
+ Subscribe to the L2 stream for a symbol in the background.
828
+
829
+ If a subscription is already active, the existing reference will be
830
+ returned; otherwise, a new subscription will be created.
831
+
832
+ Snapshots will have an initial value of timestamp=0 and bids/asks=[].
833
+
834
+ Args:
835
+ symbol: the symbol to subscribe to
836
+ venue: the marketdata venue
837
+
838
+ Return:
839
+ An L2 book object that is constantly updating in the background.
840
+ """
841
+ symbol = TradableProduct(symbol)
842
+
843
+ if venue in self.l2_books:
844
+ if symbol in self.l2_books[venue]:
845
+ return self.l2_books[venue][symbol][0]
846
+ else:
847
+ self.l2_books[venue] = {}
848
+
849
+ grpc_client = await self.marketdata(venue)
850
+ book = L2BookSnapshot([], [], 0, 0, 0, 0)
851
+ self.l2_books[venue][symbol] = (
852
+ book,
853
+ asyncio.create_task(
854
+ self.__subscribe_l2_book_task(symbol, venue, grpc_client, book)
855
+ ),
856
+ )
857
+ return book
858
+
859
+ async def __subscribe_l2_book_task(
860
+ self,
861
+ symbol: TradableProduct,
862
+ venue: Venue,
863
+ grpc_client: GrpcClient,
864
+ book: L2BookSnapshot,
865
+ ):
866
+ try:
867
+ req = SubscribeL2BookUpdatesRequest(symbol=str(symbol), venue=venue)
868
+ stream = grpc_client.unary_stream(req) # type: ignore
869
+ async for up in stream:
870
+ if isinstance(up, L2BookDiff):
871
+ if (
872
+ up.sequence_id != book.sequence_id
873
+ or up.sequence_number != book.sequence_number + 1
874
+ ):
875
+ raise ValueError(
876
+ f"Received update out-of-order for L2 book: {symbol}"
877
+ )
878
+ book.sid = up.sid
879
+ book.sn = up.sn
880
+ book.ts = up.ts
881
+ book.tn = up.tn
882
+ for px, sz in up.bids:
883
+ update_orderbook_side(book.bids, px, sz, ascending=False)
884
+ for px, sz in up.asks:
885
+ update_orderbook_side(book.asks, px, sz, ascending=True)
886
+ elif isinstance(up, L2BookSnapshot):
887
+ book.sid = up.sid
888
+ book.sn = up.sn
889
+ book.ts = up.ts
890
+ book.tn = up.tn
891
+ book.a = up.a
892
+ book.b = up.b
893
+ except Exception as e:
894
+ logging.error(
895
+ f"Error subscribing to L2 book for {symbol}, venue {venue}: {e}"
896
+ )
897
+ finally:
898
+ del self.l2_books[venue][symbol]
899
+
900
+ async def stream_trades(
901
+ self, symbol: TradableProduct | str, venue: Venue
902
+ ) -> AsyncIterator[Trade]:
903
+ """
904
+ Subscribe to a stream of trades for a symbol.
905
+ """
906
+ grpc_client = await self.marketdata(venue)
907
+ req = SubscribeTradesRequest(symbol=str(symbol), venue=venue)
908
+ return grpc_client.unary_stream(req)
909
+
910
+ async def stream_candles(
911
+ self,
912
+ symbol: TradableProduct | str,
913
+ venue: Venue,
914
+ candle_widths: Optional[list[CandleWidth]],
915
+ ) -> AsyncIterator[Candle]:
916
+ """
917
+ Subscribe to a stream of candles for a symbol.
435
918
  """
436
- user_id = await self.graphql_client.user_id_query()
919
+ grpc_client = await self.marketdata(venue)
920
+ req = SubscribeCandlesRequest(
921
+ symbol=str(symbol),
922
+ venue=venue,
923
+ candle_widths=candle_widths,
924
+ )
925
+ return grpc_client.unary_stream(req)
437
926
 
438
- return user_id.user_id, user_id.user_email
927
+ # ------------------------------------------------------------
928
+ # Portfolio management
929
+ # ------------------------------------------------------------
439
930
 
440
931
  async def list_accounts(self) -> Sequence[AccountWithPermissionsFields]:
441
932
  """
@@ -443,23 +934,35 @@ class AsyncClient:
443
934
 
444
935
  Returns:
445
936
  a list of AccountWithPermissionsFields for the user that the API key belongs to
937
+ a list of AccountWithPermissions for the user that the API key belongs to
446
938
  (use who_am_i to get the user_id / email)
447
939
  """
448
- accounts = await self.graphql_client.list_accounts_query()
940
+ res = await self.graphql_client.list_accounts_query()
941
+ return res.accounts
942
+
943
+ """
944
+ async def list_accounts(self) -> List[grpc_definitions.AccountWithPermissions]:
945
+ request = AccountsRequest()
946
+ accounts = await self.grpc_client.request(request)
449
947
  return accounts.accounts
948
+ """
450
949
 
451
950
  async def get_account_summary(self, account: str) -> AccountSummaryFields:
452
951
  """
453
- Gets the account summary for the given account.
952
+ Get account summary, including balances, positions, pnls, etc.
454
953
 
455
954
  Args:
456
- account: the account to get the summary for,
457
- can be the account id( (a UUID) or the account name (e.g. CQG:00000)
458
- Returns:
459
- AccountSummaryFields for the account
955
+ account: account uuid or name
956
+ Examples: "00000000-0000-0000-0000-000000000000", "STONEX:000000/JDoe"
957
+ """
958
+ res = await self.graphql_client.get_account_summary_query(account=account)
959
+ return res.account_summary
960
+ """
961
+ async def get_account_summary(self, account: str) -> AccountSummary:
962
+ request = AccountSummaryRequest(account=account)
963
+ account_summary = await self.grpc_client.request(request)
964
+ return account_summary
460
965
  """
461
- summary = await self.graphql_client.get_account_summary_query(account=account)
462
- return summary.account_summary
463
966
 
464
967
  async def get_account_summaries(
465
968
  self,
@@ -467,21 +970,31 @@ class AsyncClient:
467
970
  trader: Optional[str] = None,
468
971
  ) -> Sequence[AccountSummaryFields]:
469
972
  """
470
- Gets the account summaries for the given accounts and trader.
973
+ Get account summaries for accounts matching the filters.
471
974
 
472
975
  Args:
473
- accounts: a list of account ids to get summaries for,
474
- can be the account id( (a UUID) or the account name (e.g. CQG:00000)
475
- trader: the trader / userId to get summaries for
976
+ accounts: list of account uuids or names
977
+ trader: if specified, return summaries for all accounts for this trader
476
978
 
477
- if both arguments are given, the accounts are all appended and returned together
478
- Returns:
479
- a list of AccountSummary for the accounts
979
+ If both arguments are given, the union of matching accounts are returned.
480
980
  """
481
- summaries = await self.graphql_client.get_account_summaries_query(
981
+ res = await self.graphql_client.get_account_summaries_query(
482
982
  trader=trader, accounts=accounts
483
983
  )
484
- return summaries.account_summaries
984
+ return res.account_summaries
985
+ """
986
+ async def get_account_summaries(
987
+ self,
988
+ accounts: Optional[list[str]] = None,
989
+ trader: Optional[str] = None,
990
+ ) -> list[AccountSummary]:
991
+ request = AccountSummariesRequest(
992
+ accounts=accounts,
993
+ trader=trader,
994
+ )
995
+ account_summaries = await self.grpc_client.request(request)
996
+ return account_summaries.account_summaries
997
+ """
485
998
 
486
999
  async def get_account_history(
487
1000
  self,
@@ -490,19 +1003,43 @@ class AsyncClient:
490
1003
  to_exclusive: Optional[datetime] = None,
491
1004
  ) -> Sequence[AccountSummaryFields]:
492
1005
  """
493
- Gets the account history for the given account and dates.
1006
+ Get historical sequence of account summaries for the given account.
1007
+ """
1008
+ res = await self.graphql_client.get_account_history_query(
1009
+ account=account, from_inclusive=from_inclusive, to_exclusive=to_exclusive
1010
+ )
1011
+ return res.account_history
494
1012
 
495
- Returns:
496
- a list of AccountSummaryFields for the account for the given dates
497
- use timestamp to get the time of the of the summary
498
1013
  """
499
- history = await self.graphql_client.get_account_history_query(
1014
+ async def get_account_history(
1015
+ self,
1016
+ account: str,
1017
+ from_inclusive: Optional[datetime] = None,
1018
+ to_exclusive: Optional[datetime] = None,
1019
+ ) -> list[AccountSummary]:
1020
+ if from_inclusive is not None:
1021
+ assert from_inclusive.tzinfo is timezone.utc, (
1022
+ "from_inclusive must be a utc datetime:\n"
1023
+ "for example datetime.now(timezone.utc) or \n"
1024
+ "dt = datetime(2025, 4, 15, 12, 0, 0, tzinfo=timezone.utc)"
1025
+ )
1026
+
1027
+ if to_exclusive is not None:
1028
+ assert to_exclusive.tzinfo is timezone.utc, (
1029
+ "to_exclusive must be a utc datetime:\n"
1030
+ "for example datetime.now(timezone.utc) or \n"
1031
+ "dt = datetime(2025, 4, 15, 12, 0, 0, tzinfo=timezone.utc)"
1032
+ )
1033
+
1034
+ request = AccountHistoryRequest(
500
1035
  account=account, from_inclusive=from_inclusive, to_exclusive=to_exclusive
501
1036
  )
502
- return history.account_history
1037
+ history = await self.grpc_client.request(request)
1038
+ return history.history
1039
+ """
503
1040
 
504
1041
  # ------------------------------------------------------------
505
- # Order Management
1042
+ # Order management
506
1043
  # ------------------------------------------------------------
507
1044
 
508
1045
  async def get_open_orders(
@@ -525,12 +1062,10 @@ class AsyncClient:
525
1062
  symbol: the symbol to get orders for
526
1063
  parent_order_id: the parent order id to get orders for
527
1064
 
528
- these filters are combinewd via OR statements so if you pass
529
- in multiple arguments, it will return the union of the results
530
1065
  Returns:
531
- a list of OrderFields of the open orders that match the union of the filters
1066
+ Open orders that match the union of the filters
532
1067
  """
533
- orders = await self.graphql_client.get_open_orders_query(
1068
+ res = await self.graphql_client.get_open_orders_query(
534
1069
  venue=venue,
535
1070
  account=account,
536
1071
  trader=trader,
@@ -538,17 +1073,16 @@ class AsyncClient:
538
1073
  parent_order_id=parent_order_id,
539
1074
  order_ids=order_ids,
540
1075
  )
541
- return orders.open_orders
1076
+ return res.open_orders
542
1077
 
543
1078
  async def get_all_open_orders(self) -> Sequence[OrderFields]:
544
1079
  """
545
- Returns a list of all open orders for the user.
1080
+ @deprecated(reason="Use get_open_orders with no parameters instead")
546
1081
 
547
- Returns:
548
- a list of OrderFields of all the open orders for the user
1082
+ Returns a list of all open orders for the authenticated user.
549
1083
  """
550
- orders = await self.graphql_client.get_open_orders_query()
551
- return orders.open_orders
1084
+ res = await self.graphql_client.get_open_orders_query()
1085
+ return res.open_orders
552
1086
 
553
1087
  async def get_historical_orders(
554
1088
  self,
@@ -560,24 +1094,25 @@ class AsyncClient:
560
1094
  parent_order_id: Optional[str] = None,
561
1095
  ) -> Sequence[OrderFields]:
562
1096
  """
563
- Gets the historical orders that match the filters.
1097
+ Returns a list of all historical orders that match the filters.
1098
+
1099
+ Historical orders are orders that are not open, having been filled,
1100
+ canceled, expired, or outed.
564
1101
 
565
1102
  Args:
566
1103
  order_ids: a list of order ids to get
567
1104
  from_inclusive: the start date to get orders for
568
1105
  to_exclusive: the end date to get orders for
569
1106
  venue: the venue to get orders for, e.g. CME
570
- account: the account to get orders for,
571
- can be the account id( (a UUID) or the account name (e.g. CQG:00000)
1107
+ account: account uuid or name
572
1108
  parent_order_id: the parent order id to get orders for
573
1109
  Returns:
574
- a list of OrderFields of the historical orders that match the filters
1110
+ Historical orders that match the union of the filters.
575
1111
 
576
- either the order_ids parameter needs to be filled
577
- OR
578
- the from_inclusive and to_exclusive parameters need to be filled
1112
+ If order_ids is not specified, then from_inclusive and to_exclusive
1113
+ MUST be specified.
579
1114
  """
580
- orders = await self.graphql_client.get_historical_orders_query(
1115
+ res = await self.graphql_client.get_historical_orders_query(
581
1116
  order_ids=order_ids,
582
1117
  venue=venue,
583
1118
  account=account,
@@ -585,54 +1120,41 @@ class AsyncClient:
585
1120
  from_inclusive=from_inclusive,
586
1121
  to_exclusive=to_exclusive,
587
1122
  )
588
- return orders.historical_orders
1123
+ return res.historical_orders
589
1124
 
590
1125
  async def get_order(self, order_id: str) -> Optional[OrderFields]:
591
1126
  """
592
- Returns the OrderFields object for the specified order.
593
- Useful for looking at past sent orders.
1127
+ Returns the specified order. Useful for looking at past sent orders.
1128
+ Queries open_orders first, then queries historical_orders.
594
1129
 
595
1130
  Args:
596
1131
  order_id: the order id to get
597
- Returns:
598
- the OrderFields object for the order
599
-
600
- Queries open_orders first then queries historical_orders
601
1132
  """
602
- open_orders = await self.graphql_client.get_open_orders_query(
603
- order_ids=[order_id]
604
- )
605
-
606
- for open_order in open_orders.open_orders:
1133
+ res = await self.graphql_client.get_open_orders_query(order_ids=[order_id])
1134
+ for open_order in res.open_orders:
607
1135
  if open_order.id == order_id:
608
1136
  return open_order
609
1137
 
610
- historical_orders = await self.graphql_client.get_historical_orders_query(
1138
+ res = await self.graphql_client.get_historical_orders_query(
611
1139
  order_ids=[order_id]
612
1140
  )
613
-
614
- if historical_orders.historical_orders:
615
- return historical_orders.historical_orders[0]
1141
+ if res.historical_orders and len(res.historical_orders) > 0:
1142
+ return res.historical_orders[0]
616
1143
 
617
1144
  async def get_orders(self, order_ids: list[str]) -> list[Optional[OrderFields]]:
618
1145
  """
619
- Returns a list of OrderFields objects for the specified orders.
620
- Useful for looking at past sent orders.
1146
+ Returns the specified orders. Useful for looking at past sent orders.
1147
+ Plural form of get_order.
621
1148
 
622
1149
  Args:
623
1150
  order_ids: a list of order ids to get
624
- Returns:
625
- a list of OrderFields objects for the orders
626
-
627
- Plural form of get_order
628
1151
  """
629
1152
  orders_dict: dict[str, Optional[OrderFields]] = {
630
1153
  order_id: None for order_id in order_ids
631
1154
  }
632
1155
 
633
- open_orders = (
634
- await self.graphql_client.get_open_orders_query(order_ids=order_ids)
635
- ).open_orders
1156
+ res = await self.graphql_client.get_open_orders_query(order_ids=order_ids)
1157
+ open_orders = res.open_orders
636
1158
  for open_order in open_orders:
637
1159
  orders_dict[open_order.id] = open_order
638
1160
 
@@ -640,11 +1162,10 @@ class AsyncClient:
640
1162
  order_id for order_id in order_ids if orders_dict[order_id] is None
641
1163
  ]
642
1164
 
643
- historical_orders = (
644
- await self.graphql_client.get_historical_orders_query(
645
- order_ids=not_open_order_ids
646
- )
647
- ).historical_orders
1165
+ res = await self.graphql_client.get_historical_orders_query(
1166
+ order_ids=not_open_order_ids
1167
+ )
1168
+ historical_orders = res.historical_orders
648
1169
  for historical_order in historical_orders:
649
1170
  orders_dict[historical_order.id] = historical_order
650
1171
 
@@ -652,311 +1173,96 @@ class AsyncClient:
652
1173
 
653
1174
  async def get_fills(
654
1175
  self,
655
- from_inclusive: Optional[datetime],
656
- to_exclusive: Optional[datetime],
1176
+ from_inclusive: Optional[datetime] = None,
1177
+ to_exclusive: Optional[datetime] = None,
657
1178
  venue: Optional[str] = None,
658
1179
  account: Optional[str] = None,
659
1180
  order_id: Optional[str] = None,
660
1181
  ) -> GetFillsQueryFolioHistoricalFills:
661
1182
  """
662
- Returns a list of fills for the given filters.
1183
+ Returns all fills matching the given filters.
663
1184
 
664
1185
  Args:
665
1186
  from_inclusive: the start date to get fills for
666
1187
  to_exclusive: the end date to get fills for
667
1188
  venue: the venue to get fills for, e.g. "CME"
668
- account: the account to get fills for,
669
- can be the account id( (a UUID) or the account name (e.g. CQG:00000)
1189
+ account: account uuid or name
670
1190
  order_id: the order id to get fills for
671
- Returns:
672
- a list of GetFillsQueryFolioHistoricalFills
673
1191
  """
674
- fills = await self.graphql_client.get_fills_query(
1192
+ res = await self.graphql_client.get_fills_query(
675
1193
  venue, account, order_id, from_inclusive, to_exclusive
676
1194
  )
677
- return fills.historical_fills
678
-
679
- # ------------------------------------------------------------
680
- # Market Data
681
- # ------------------------------------------------------------
682
-
683
- async def get_market_status(
684
- self, symbol: TradableProduct, venue: str
685
- ) -> MarketStatusFields:
686
- """
687
- Returns market status for symbol (ie if it is quoting and trading).
1195
+ return res.historical_fills
688
1196
 
689
- Args:
690
- symbol: the symbol to get the market status for, e.g. "ES 20250321 CME Future/USD"
691
- venue: the venue that the symbol is traded at, e.g. CME
692
- Returns:
693
- MarketStatusFields for the symbol
694
- """
695
- market_status = await self.graphql_client.get_market_status_query(symbol, venue)
696
- return market_status.market_status
697
-
698
- async def get_market_snapshot(
699
- self, symbol: TradableProduct, venue: str
700
- ) -> MarketTickerFields:
701
- """
702
- This is an alias for l1_book_snapshot.
703
-
704
- Args:
705
- symbol: the symbol to get the market snapshot for, e.g. "ES 20250321 CME Future/USD"
706
- venue: the venue that the symbol is traded at, e.g. CME
707
- Returns:
708
- MarketTickerFields for the symbol
709
- """
710
- return await self.get_l1_book_snapshot(symbol=symbol, venue=venue)
711
-
712
- async def get_market_snapshots(
713
- self, symbols: list[TradableProduct], venue: str
714
- ) -> Sequence[MarketTickerFields]:
715
- """
716
- This is an alias for l1_book_snapshot.
717
-
718
- Args:
719
- symbols: the symbols to get the market snapshots for
720
- venue: the venue that the symbols are traded at
721
- Returns:
722
- a list of MarketTickerFields for the symbols
723
- """
724
- return await self.get_l1_book_snapshots(
725
- venue=venue, symbols=symbols # type: ignore
726
- )
727
-
728
- async def get_historical_candles(
1197
+ async def orderflow(
729
1198
  self,
730
- symbol: str,
731
- candle_width: grpc_definitions.CandleWidth,
732
- start: datetime,
733
- end: datetime,
734
- ) -> HistoricalCandlesResponse:
1199
+ request_iterator: AsyncIterator[OrderflowRequest],
1200
+ ) -> AsyncIterator[Orderflow]:
735
1201
  """
736
- Gets the historical candles for a symbol.
1202
+ A two-way channel for both order entry and listening to order updates (fills, acks, outs, etc.).
737
1203
 
738
- Args:
739
- symbol: the symbol to get the candles for
740
- venue: the venue of the symbol
741
- candle_width: the width of the candles
742
- start: the start date to get candles for
743
- end: the end date to get candles for
744
- Returns:
745
- a list of CandleFields for the specified candles
746
- """
747
- request = HistoricalCandlesRequest(
748
- symbol=symbol,
749
- candle_width=candle_width,
750
- start_date=start,
751
- end_date=end,
752
- )
1204
+ This is considered the most efficient way to trade in this SDK.
753
1205
 
754
- return await self.grpc_client.request(request)
1206
+ Example:
1207
+ See test_orderflow.py for an example.
755
1208
 
756
- async def get_l1_book_snapshot(
757
- self,
758
- symbol: str,
759
- venue: str,
760
- ) -> MarketTickerFields:
1209
+ This WILL block the event loop until the stream is closed.
761
1210
  """
762
- Gets the L1 book snapshot for a symbol.
763
-
764
- Args:
765
- symbol: the symbol to get the l1 book snapshot for
766
- venue: the venue that the symbol is traded at
767
- Returns:
768
- MarketTickerFields for the symbol
769
- """
770
- snapshot = await self.graphql_client.get_l_1_book_snapshot_query(
771
- symbol=symbol, venue=venue
1211
+ grpc_client = await self.core()
1212
+ decoder = grpc_client.get_decoder(OrderflowRequestUnannotatedResponseType)
1213
+ stub = grpc_client.channel.stream_stream(
1214
+ OrderflowRequest_route,
1215
+ request_serializer=grpc_client.encoder().encode,
1216
+ response_deserializer=decoder.decode,
772
1217
  )
773
- return snapshot.ticker
774
-
775
- async def get_l1_book_snapshots(
776
- self, symbols: list[str], venue: str
777
- ) -> Sequence[MarketTickerFields]:
778
- """
779
- Gets the L1 book snapshots for a list of symbols.
780
-
781
- Args:
782
- symbols: the symbols to get the l1 book snapshots for
783
- venue: the venue that the symbols are traded at
784
- Returns:
785
- a list of MarketTickerFields for the symbols
786
- """
787
- snapshot = await self.graphql_client.get_l_1_book_snapshots_query(
788
- venue=venue, symbols=symbols
789
- )
790
- return snapshot.tickers
791
-
792
- async def get_l2_book_snapshot(self, symbol: str, venue: str) -> L2BookFields:
793
- """
794
- Gets the L2 book snapshot for a symbol.
795
-
796
- Args:
797
- symbol: the symbol to get the l2 book snapshot for
798
- venue: the venue that the symbol is traded at
799
- Returns:
800
- L2BookFields for the symbol
801
-
802
- Note: this does NOT update, it is a snapshot at a given time
803
- For an object that updates, use subscribe_l2_book
804
- """
805
- l2_book = await self.graphql_client.get_l_2_book_snapshot_query(
806
- symbol=symbol, venue=venue
1218
+ call = stub(
1219
+ request_iterator, metadata=(("authorization", f"Bearer {grpc_client.jwt}"),)
807
1220
  )
808
- return l2_book.l_2_book_snapshot
809
-
810
- async def subscribe_l1_book_stream(
811
- self, symbols: list[TradableProduct], venue: str
812
- ) -> AsyncIterator[L1BookSnapshot]:
813
- """
814
- Subscribe to the stream of L1BookSnapshots for a symbol.
815
-
816
- Args:
817
- symbol: the symbol to subscribe to
818
- venue: the venue to subscribe to
819
- Returns:
820
- an async iterator that yields L1BookSnapshot, representing the l1 book updates
821
- """
822
- async for snapshot in await self.grpc_client.subscribe_l1_books_stream(
823
- symbols=[str(s) for s in symbols]
824
- ):
825
- yield snapshot
826
-
827
- async def subscribe_l2_book_stream(
828
- self, symbol: TradableProduct, venue: str
829
- ) -> AsyncIterator[L2BookUpdate]:
830
- """
831
- Subscribe to the stream of L2BookUpdates for a symbol.
832
-
833
- IMPORTANT: note that the Snapshot is a different type than
834
- L2BookSnapshot
835
- Args:
836
- symbol: the symbol to subscribe to
837
- venue: the venue to subscribe to
838
- Returns:
839
- an async iterator that yields L2BookFields
840
- L2BookFields is either a Snapshot or a Diff
841
- See the grpc_client code for how to handle the different types
842
- """
843
- async for snapshot in self.grpc_client.subscribe_l2_books_stream(
844
- symbol=symbol, venue=venue
845
- ):
846
- yield snapshot
1221
+ async for update in call:
1222
+ yield update
847
1223
 
848
- async def subscribe_l1_book(
849
- self, symbols: list[TradableProduct]
850
- ) -> list[L1BookSnapshot]:
851
- """
852
- Returns a L1BookSnapshot object that is constantly updating in the background.
853
-
854
- Args:
855
- symbols: the symbols to subscribe to
856
- Return:
857
- a list of L1BookSnapshot objects that are constantly updating in the background
858
- For the duration of the program, the client will be subscribed to the stream
859
- and be updating the L1BookSnapshot.
860
-
861
- IMPORTANT: The L1BookSnapshot will be initialized with
862
- a timestamp (field tn and ts) of 0
863
- along with None for bid and ask
864
-
865
- The reference to the object should be kept, but can also be referenced via
866
- client.grpc_client.l1_books.get(symbol)
867
-
868
- If you want direct access to the stream to do on_update type code, you can
869
- call client.grpc_client.stream_l1_books
870
- """
871
- books = self.grpc_client.initialize_l1_books(symbols)
872
- asyncio.create_task(self.grpc_client.watch_l1_books(symbols=symbols))
873
- i = 0
874
- while not all(book.ts > 0 for book in books) and i < 10:
875
- await asyncio.sleep(0.2)
876
- i += 1
877
- if i == 10:
878
- raise ValueError(
879
- f"Could not get L1 books for {symbols}. Check if market is quoting via client.get_market_status."
880
- )
881
-
882
- return books
883
-
884
- async def subscribe_l2_book(
1224
+ async def stream_orderflow(
885
1225
  self,
886
- symbol: TradableProduct,
887
- venue: Optional[str],
888
- ) -> L2BookSnapshot:
1226
+ account: Optional[grpc_definitions.AccountIdOrName] = None,
1227
+ execution_venue: Optional[str] = None,
1228
+ trader: Optional[grpc_definitions.TraderIdOrEmail] = None,
1229
+ ) -> AsyncIterator[Orderflow]:
889
1230
  """
890
- Returns a L2BookSnapshot object that is constantly updating in the background.
1231
+ A stream for listening to order updates (fills, acks, outs, etc.).
891
1232
 
892
- Args:
893
- symbols: the symbols to subscribe to
894
- Return:
895
- a list of L2BookSnapshot object that is constantly updating in the background
896
- For the duration of the program, the client will be subscribed to the stream
897
- and be updating the L2BookSnapshot.
898
-
899
- IMPORTANT: The LBBookSnapshot will be initialized with
900
- a timestamp (field tn and ts) of 0
901
- along with None for bid and ask
902
-
903
- The reference to the object should be kept, but can also be referenced via
904
- client.grpc_client.l1_books.get(symbol)
905
-
906
- If you want direct access to the stream to do on_update type code, you can
907
- call client.grpc_client.stream_l2_book
908
- """
909
- book = self.grpc_client.initialize_l2_book(symbol, venue)
910
- asyncio.create_task(self.grpc_client.watch_l2_book(symbol, venue))
911
- i = 0
912
- while book.ts == 0 and i < 10:
913
- await asyncio.sleep(0.2)
914
- i += 1
915
- if book.ts == 0:
916
- if venue:
917
- market_status = await self.get_market_status(symbol, venue)
918
- if not market_status.is_quoting:
919
- raise ValueError(
920
- f"Market {symbol} is currently closed, cannot get L2."
921
- )
922
- raise ValueError(f"Could not get L2 book for {symbol}.")
923
-
924
- return book
1233
+ Example:
1234
+ ```python
1235
+ request = SubscribeOrderflowRequest.new()
1236
+ async for of in client.subscribe_orderflow_stream(request):
1237
+ print(of)
1238
+ ```
925
1239
 
926
- def subscribe_trades_stream(
927
- self, symbol: TradableProduct, venue: Optional[str]
928
- ) -> AsyncIterator[Trade]:
929
- """
930
- Subscribe to a stream of trades for a symbol.
1240
+ This WILL block the event loop until the stream is closed.
931
1241
  """
932
- request = SubscribeTradesRequest(symbol=symbol, venue=venue)
933
- return self.grpc_client.subscribe(request)
934
-
935
- def subscribe_candles_stream(
936
- self,
937
- symbol: TradableProduct,
938
- venue: Optional[str],
939
- candle_widths: Optional[list[grpc_definitions.CandleWidth]],
940
- ) -> AsyncIterator[Candle]:
941
- """
942
- Subscribe to a stream of candles for a symbol.
943
- """
944
- request = SubscribeCandlesRequest(
945
- symbol=str(symbol),
946
- venue=venue,
947
- candle_widths=candle_widths,
1242
+ grpc_client = await self.core()
1243
+ request: SubscribeOrderflowRequest = SubscribeOrderflowRequest(
1244
+ account=account, execution_venue=execution_venue, trader=trader
1245
+ )
1246
+ grpc_client = await self.core()
1247
+ decoder = grpc_client.get_decoder(SubscribeOrderflowRequest)
1248
+ stub = grpc_client.channel.unary_stream(
1249
+ SubscribeOrderflowRequest.get_route(),
1250
+ request_serializer=grpc_client.encoder().encode,
1251
+ response_deserializer=decoder.decode,
948
1252
  )
949
- return self.grpc_client.subscribe(request)
1253
+ call = stub(request, metadata=(("authorization", f"Bearer {grpc_client.jwt}"),))
1254
+ async for update in call:
1255
+ yield update
950
1256
 
951
1257
  # ------------------------------------------------------------
952
- # Order Entry and Cancellation
1258
+ # Order entry
953
1259
  # ------------------------------------------------------------
954
1260
 
955
- async def send_limit_order(
1261
+ async def place_limit_order(
956
1262
  self,
957
1263
  *,
958
1264
  id: Optional[str] = None,
959
- symbol: TradableProduct,
1265
+ symbol: TradableProduct | str,
960
1266
  execution_venue: Optional[str],
961
1267
  odir: OrderDir,
962
1268
  quantity: Decimal,
@@ -1050,7 +1356,7 @@ class AsyncClient:
1050
1356
  self,
1051
1357
  *,
1052
1358
  id: Optional[str] = None,
1053
- symbol: TradableProduct,
1359
+ symbol: TradableProduct | str,
1054
1360
  execution_venue: str,
1055
1361
  odir: OrderDir,
1056
1362
  quantity: Decimal,
@@ -1078,36 +1384,33 @@ class AsyncClient:
1078
1384
  If the order is rejected, the order.reject_reason and order.reject_message will be set
1079
1385
  """
1080
1386
 
1081
- # Check for GQL failures
1082
- bbo_snapshot = await self.get_market_snapshot(
1083
- symbol=symbol, venue=execution_venue
1084
- )
1085
- if bbo_snapshot is None:
1387
+ ticker = await self.get_ticker(symbol, execution_venue)
1388
+ if ticker is None:
1086
1389
  raise ValueError(
1087
- f"Failed to send market order with reason: no market snapshot for {symbol}"
1390
+ f"Failed to send market order with reason: no ticker for {symbol}"
1088
1391
  )
1089
1392
 
1090
1393
  price_band = price_band_pairs.get(symbol, None)
1091
1394
 
1092
1395
  if odir == OrderDir.BUY:
1093
- if bbo_snapshot.ask_price is None:
1396
+ if ticker.ask_price is None:
1094
1397
  raise ValueError(
1095
1398
  f"Failed to send market order with reason: no ask price for {symbol}"
1096
1399
  )
1097
- limit_price = bbo_snapshot.ask_price * (1 + fraction_through_market)
1400
+ limit_price = ticker.ask_price * (1 + fraction_through_market)
1098
1401
 
1099
- if price_band and bbo_snapshot.last_price:
1100
- price_band_reference_price = bbo_snapshot.last_price + price_band
1402
+ if price_band and ticker.last_price:
1403
+ price_band_reference_price = ticker.last_price + price_band
1101
1404
  limit_price = min(limit_price, price_band_reference_price)
1102
1405
 
1103
1406
  else:
1104
- if bbo_snapshot.bid_price is None:
1407
+ if ticker.bid_price is None:
1105
1408
  raise ValueError(
1106
1409
  f"Failed to send market order with reason: no bid price for {symbol}"
1107
1410
  )
1108
- limit_price = bbo_snapshot.bid_price * (1 - fraction_through_market)
1109
- if price_band and bbo_snapshot.last_price:
1110
- price_band_reference_price = bbo_snapshot.last_price - price_band
1411
+ limit_price = ticker.bid_price * (1 - fraction_through_market)
1412
+ if price_band and ticker.last_price:
1413
+ price_band_reference_price = ticker.last_price - price_band
1111
1414
  limit_price = min(limit_price, price_band_reference_price)
1112
1415
 
1113
1416
  # Conservatively round price to nearest tick
@@ -1125,7 +1428,7 @@ class AsyncClient:
1125
1428
  ):
1126
1429
  limit_price = tick_round_method(limit_price, tick_size)
1127
1430
 
1128
- return await self.send_limit_order(
1431
+ return await self.place_limit_order(
1129
1432
  id=id,
1130
1433
  symbol=symbol,
1131
1434
  execution_venue=execution_venue,