architect-py 3.2.1__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 (171) hide show
  1. architect_py/__init__.py +8 -2
  2. architect_py/async_client.py +887 -575
  3. architect_py/client.py +36 -33
  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 +5 -4
  31. architect_py/{grpc_client → grpc/models}/Cpty/CptyResponse.py +6 -6
  32. architect_py/grpc/models/Cpty/CptyStatus.py +48 -0
  33. architect_py/grpc/models/Cpty/CptyStatusRequest.py +45 -0
  34. architect_py/grpc/models/Cpty/CptysRequest.py +37 -0
  35. architect_py/grpc/models/Cpty/CptysResponse.py +27 -0
  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 +14 -3
  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 +2 -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 +293 -223
  108. architect_py/grpc/resolve_endpoint.py +67 -0
  109. architect_py/{grpc_client/grpc_server.py → grpc/server.py} +13 -9
  110. architect_py/grpc/utils.py +32 -0
  111. architect_py/tests/conftest.py +86 -85
  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.1.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 -52
  148. architect_py/grpc_client/Folio/AggregatedAccountSummariesRequest.py +0 -59
  149. architect_py/grpc_client/Folio/AggregatedAccountSummariesResponse.py +0 -27
  150. architect_py/grpc_client/Health/__init__.py +0 -2
  151. architect_py/grpc_client/Marketdata/__init__.py +0 -2
  152. architect_py/grpc_client/Oms/__init__.py +0 -2
  153. architect_py/grpc_client/Orderflow/__init__.py +0 -2
  154. architect_py/grpc_client/Symbology/__init__.py +0 -2
  155. architect_py/grpc_client/__init__.py +0 -2
  156. architect_py/grpc_client/grpc_client.py +0 -405
  157. architect_py/scalars.py +0 -172
  158. architect_py/tests/test_accounts.py +0 -31
  159. architect_py/tests/test_client.py +0 -29
  160. architect_py/tests/test_grpc_client.py +0 -30
  161. architect_py/tests/test_order_sending.py +0 -61
  162. architect_py/tests/test_snapshots.py +0 -52
  163. architect_py/tests/test_subscriptions.py +0 -129
  164. architect_py-3.2.1.dist-info/METADATA +0 -212
  165. architect_py-3.2.1.dist-info/RECORD +0 -146
  166. /architect_py/{grpc_client → grpc/models}/Marketdata/ArrayOfL1BookSnapshot.py +0 -0
  167. /architect_py/{grpc_client → grpc/models}/Marketdata/L2BookUpdate.py +0 -0
  168. /architect_py/{grpc_client → grpc/models}/Marketdata/TickerUpdate.py +0 -0
  169. /architect_py/{grpc_client → grpc/models}/Orderflow/Dropcopy.py +0 -0
  170. /architect_py/{grpc_client → grpc/models}/Orderflow/Orderflow.py +0 -0
  171. {architect_py-3.2.1.dist-info → architect_py-5.0.0b1.dist-info/licenses}/LICENSE +0 -0
@@ -1,208 +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 not be using this constructor directly, unless they do not want to use any subscription methods.
165
-
166
- Use the create 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
210
+
211
+ async def discover_marketdata(self):
212
+ """
213
+ Load marketdata endpoints from the server config.
214
+
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.
220
+ """
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
201
320
 
202
321
  # ------------------------------------------------------------
203
322
  # Symbology
204
323
  # ------------------------------------------------------------
205
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
+
206
343
  async def search_symbols(
207
344
  self,
208
345
  search_string: Optional[str] = None,
@@ -211,77 +348,70 @@ class AsyncClient:
211
348
  limit: int = 20,
212
349
  ) -> List[TradableProduct]:
213
350
  """
214
- Search for symbols in the Architect database.
351
+ Search for tradable products on Architect.
215
352
 
216
353
  Args:
217
- 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.
218
356
  Examples: "ES", "NQ", "GC"
219
357
  execution_venue: the execution venue to search in
220
358
  Examples: "CME"
221
- Returns:
222
- a list of TradableProduct objects
223
- """
224
- markets = (
225
- await self.graphql_client.search_symbols_query(
226
- search_string=search_string,
227
- execution_venue=execution_venue,
228
- offset=offset,
229
- limit=limit,
230
- )
231
- ).search_symbols
232
-
233
- 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
234
367
 
235
368
  async def get_product_info(self, symbol: str) -> Optional[ProductInfoFields]:
236
369
  """
237
- Get the product information (product_type, underlying, multiplier, etc.) for a symbol.
370
+ Get information about a product, e.g. product_type, underlying, multiplier.
238
371
 
239
372
  Args:
240
373
  symbol: the symbol to get information for
374
+
241
375
  Returns:
242
- ProductInfoFields object if the symbol exists
376
+ None if the symbol does not exist
243
377
  """
244
- info = await self.graphql_client.get_product_info_query(symbol)
245
- return info.product_info
378
+ res = await self.graphql_client.get_product_info_query(symbol)
379
+ return res.product_info
246
380
 
247
- @functools.lru_cache
248
381
  async def get_product_infos(
249
382
  self, symbols: Optional[list[str]]
250
383
  ) -> Sequence[ProductInfoFields]:
251
384
  """
252
- 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.
253
386
 
254
387
  Args:
255
- symbols: the symbols to get information for
256
- Returns:
257
- a list of ProductInfoFields
388
+ symbols: the symbols to get information for, or None for all symbols
258
389
 
259
- Any duplicate or invalid symbols will be ignored.
260
- The order of the symbols in the list will not necessarily be preserved in the output.
261
-
262
- 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.
263
394
  """
264
- infos = await self.graphql_client.get_product_infos_query(symbols)
265
- return infos.product_infos
395
+ res = await self.graphql_client.get_product_infos_query(symbols)
396
+ return res.product_infos
266
397
 
267
- @functools.lru_cache
268
398
  async def get_execution_info(
269
- self, symbol: TradableProduct, execution_venue: str
399
+ self, symbol: TradableProduct | str, execution_venue: str
270
400
  ) -> Optional[ExecutionInfoFields]:
271
401
  """
272
- 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.
273
404
 
274
405
  Args:
275
406
  symbol: the symbol to get execution information for
276
407
  execution_venue: the execution venue e.g. "CME"
277
408
 
278
409
  Returns:
279
- ExecutionInfoFields
280
-
410
+ None if the symbol doesn't exist
281
411
  """
282
412
  try:
283
413
  execution_info = await self.graphql_client.get_execution_info_query(
284
- symbol, execution_venue
414
+ TradableProduct(symbol), execution_venue
285
415
  )
286
416
  return execution_info.execution_info
287
417
  except GraphQLClientGraphQLMultiError:
@@ -295,59 +425,60 @@ class AsyncClient:
295
425
  execution_venue: Optional[str] = None,
296
426
  ) -> Sequence[ExecutionInfoFields]:
297
427
  """
298
- 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.
299
430
 
300
431
  Args:
301
- symbols: the symbols to get execution information for
432
+ symbols: the symbols to get execution information for, or None for all symbols
302
433
  execution_venue: the execution venue e.g. "CME"
303
434
 
304
435
  Returns:
305
- a list of ExecutionInfoFields
306
-
307
- 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.
308
439
  """
309
- execution_infos = await self.graphql_client.get_execution_infos_query(
440
+ res = await self.graphql_client.get_execution_infos_query(
310
441
  symbols, execution_venue
311
442
  )
312
- return execution_infos.execution_infos
443
+ return res.execution_infos
313
444
 
314
445
  async def get_cme_first_notice_date(self, symbol: str) -> Optional[date]:
315
446
  """
447
+ @deprecated(reason="Use get_product_info instead; first_notice_date is now a field")
448
+
316
449
  Get the first notice date for a CME future.
317
450
 
318
451
  Args:
319
452
  symbol: the symbol to get the first notice date for a CME future
320
453
 
321
454
  Returns:
322
- the first notice date as a date object if it exists
455
+ The first notice date as a date object if it exists
323
456
  """
324
- notice = await self.graphql_client.get_first_notice_date_query(symbol)
325
- 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:
326
459
  return None
327
- return notice.product_info.first_notice_date
460
+ return res.product_info.first_notice_date
328
461
 
329
- async def get_future_series(self, series_symbol: str) -> list[str]:
462
+ async def get_futures_series(self, series_symbol: str) -> list[str]:
330
463
  """
331
- Get the series of futures for a given series symbol.
464
+ List all futures in a given series.
332
465
 
333
466
  Args:
334
- series_symbol: the symbol to get the series for
335
- 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
336
469
  Returns:
337
- a list of symbols in the series
470
+ List of futures products
338
471
  """
339
- assert (
340
- " " in series_symbol
341
- ), 'series_symbol must have the venue in it, e.g. "ES CME Futures" or "GC CME Futures"'
342
-
343
- futures_series = await self.graphql_client.get_future_series_query(
344
- series_symbol
345
- )
346
- 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
347
476
 
348
477
  @staticmethod
349
- def get_expiration_from_CME_name(name: str) -> date:
478
+ def get_expiration_from_CME_name(name: str) -> Optional[date]:
350
479
  """
480
+ @deprecated(reason="Use utils.symbol_parsing.nominative_expiration instead")
481
+
351
482
  Get the expiration date from a CME future name.
352
483
 
353
484
  Args:
@@ -356,12 +487,13 @@ class AsyncClient:
356
487
  Returns:
357
488
  the expiration date as a date object
358
489
  """
359
- _, d, *_ = name.split(" ")
360
- return datetime.strptime(d, "%Y%m%d").date()
490
+ return nominative_expiration(name)
361
491
 
362
492
  async def get_cme_futures_series(self, series: str) -> list[tuple[date, str]]:
363
493
  """
364
- 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.
365
497
 
366
498
  Args:
367
499
  series: the series to get the futures for
@@ -377,33 +509,28 @@ class AsyncClient:
377
509
  ...
378
510
  ]
379
511
  """
380
- markets = await self.get_future_series(
381
- series,
382
- )
383
-
384
- filtered_markets = [
385
- (self.get_expiration_from_CME_name(market), market) for market in markets
386
- ]
387
-
388
- filtered_markets.sort(key=lambda x: x[0])
389
-
390
- 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
391
520
 
392
521
  async def get_cme_future_from_root_month_year(
393
522
  self, root: str, month: int, year: int
394
523
  ) -> str:
395
524
  """
396
525
  Get the symbol for a CME future from the root, month, and year.
526
+ This is a simple wrapper around search_symbols.
397
527
 
398
528
  Args:
399
529
  root: the root symbol for the future e.g. "ES"
400
530
  month: the month of the future
401
531
  year: the year of the future
402
532
  Returns:
403
- the symbol for the future
404
-
405
- Errors if the result is not unique
406
- This is a simple wrapper around search_symbols
533
+ The future symbol if it exists and is unique.
407
534
  """
408
535
  [market] = [
409
536
  market
@@ -413,23 +540,393 @@ class AsyncClient:
413
540
  )
414
541
  if market.startswith(f"{root} {year}{month:02d}")
415
542
  ]
416
-
417
543
  return market
418
544
 
419
545
  # ------------------------------------------------------------
420
- # Account Management
546
+ # Marketdata
421
547
  # ------------------------------------------------------------
422
548
 
423
- async def who_am_i(self) -> tuple[str, str]:
549
+ async def get_market_status(
550
+ self, symbol: TradableProduct | str, venue: Venue
551
+ ) -> MarketStatus:
424
552
  """
425
- 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
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.
426
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
427
575
  Returns:
428
- (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.
429
918
  """
430
- 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)
431
926
 
432
- return user_id.user_id, user_id.user_email
927
+ # ------------------------------------------------------------
928
+ # Portfolio management
929
+ # ------------------------------------------------------------
433
930
 
434
931
  async def list_accounts(self) -> Sequence[AccountWithPermissionsFields]:
435
932
  """
@@ -437,23 +934,35 @@ class AsyncClient:
437
934
 
438
935
  Returns:
439
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
440
938
  (use who_am_i to get the user_id / email)
441
939
  """
442
- 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)
443
947
  return accounts.accounts
948
+ """
444
949
 
445
950
  async def get_account_summary(self, account: str) -> AccountSummaryFields:
446
951
  """
447
- Gets the account summary for the given account.
952
+ Get account summary, including balances, positions, pnls, etc.
448
953
 
449
954
  Args:
450
- account: the account to get the summary for,
451
- can be the account id( (a UUID) or the account name (e.g. CQG:00000)
452
- Returns:
453
- 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
454
965
  """
455
- summary = await self.graphql_client.get_account_summary_query(account=account)
456
- return summary.account_summary
457
966
 
458
967
  async def get_account_summaries(
459
968
  self,
@@ -461,21 +970,31 @@ class AsyncClient:
461
970
  trader: Optional[str] = None,
462
971
  ) -> Sequence[AccountSummaryFields]:
463
972
  """
464
- Gets the account summaries for the given accounts and trader.
973
+ Get account summaries for accounts matching the filters.
465
974
 
466
975
  Args:
467
- accounts: a list of account ids to get summaries for,
468
- can be the account id( (a UUID) or the account name (e.g. CQG:00000)
469
- 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
470
978
 
471
- if both arguments are given, the accounts are all appended and returned together
472
- Returns:
473
- a list of AccountSummary for the accounts
979
+ If both arguments are given, the union of matching accounts are returned.
474
980
  """
475
- summaries = await self.graphql_client.get_account_summaries_query(
981
+ res = await self.graphql_client.get_account_summaries_query(
476
982
  trader=trader, accounts=accounts
477
983
  )
478
- 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
+ """
479
998
 
480
999
  async def get_account_history(
481
1000
  self,
@@ -484,19 +1003,43 @@ class AsyncClient:
484
1003
  to_exclusive: Optional[datetime] = None,
485
1004
  ) -> Sequence[AccountSummaryFields]:
486
1005
  """
487
- 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
488
1012
 
489
- Returns:
490
- a list of AccountSummaryFields for the account for the given dates
491
- use timestamp to get the time of the of the summary
492
1013
  """
493
- 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(
494
1035
  account=account, from_inclusive=from_inclusive, to_exclusive=to_exclusive
495
1036
  )
496
- return history.account_history
1037
+ history = await self.grpc_client.request(request)
1038
+ return history.history
1039
+ """
497
1040
 
498
1041
  # ------------------------------------------------------------
499
- # Order Management
1042
+ # Order management
500
1043
  # ------------------------------------------------------------
501
1044
 
502
1045
  async def get_open_orders(
@@ -519,12 +1062,10 @@ class AsyncClient:
519
1062
  symbol: the symbol to get orders for
520
1063
  parent_order_id: the parent order id to get orders for
521
1064
 
522
- these filters are combinewd via OR statements so if you pass
523
- in multiple arguments, it will return the union of the results
524
1065
  Returns:
525
- 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
526
1067
  """
527
- orders = await self.graphql_client.get_open_orders_query(
1068
+ res = await self.graphql_client.get_open_orders_query(
528
1069
  venue=venue,
529
1070
  account=account,
530
1071
  trader=trader,
@@ -532,17 +1073,16 @@ class AsyncClient:
532
1073
  parent_order_id=parent_order_id,
533
1074
  order_ids=order_ids,
534
1075
  )
535
- return orders.open_orders
1076
+ return res.open_orders
536
1077
 
537
1078
  async def get_all_open_orders(self) -> Sequence[OrderFields]:
538
1079
  """
539
- Returns a list of all open orders for the user.
1080
+ @deprecated(reason="Use get_open_orders with no parameters instead")
540
1081
 
541
- Returns:
542
- a list of OrderFields of all the open orders for the user
1082
+ Returns a list of all open orders for the authenticated user.
543
1083
  """
544
- orders = await self.graphql_client.get_open_orders_query()
545
- return orders.open_orders
1084
+ res = await self.graphql_client.get_open_orders_query()
1085
+ return res.open_orders
546
1086
 
547
1087
  async def get_historical_orders(
548
1088
  self,
@@ -554,24 +1094,25 @@ class AsyncClient:
554
1094
  parent_order_id: Optional[str] = None,
555
1095
  ) -> Sequence[OrderFields]:
556
1096
  """
557
- 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.
558
1101
 
559
1102
  Args:
560
1103
  order_ids: a list of order ids to get
561
1104
  from_inclusive: the start date to get orders for
562
1105
  to_exclusive: the end date to get orders for
563
1106
  venue: the venue to get orders for, e.g. CME
564
- account: the account to get orders for,
565
- can be the account id( (a UUID) or the account name (e.g. CQG:00000)
1107
+ account: account uuid or name
566
1108
  parent_order_id: the parent order id to get orders for
567
1109
  Returns:
568
- a list of OrderFields of the historical orders that match the filters
1110
+ Historical orders that match the union of the filters.
569
1111
 
570
- either the order_ids parameter needs to be filled
571
- OR
572
- 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.
573
1114
  """
574
- orders = await self.graphql_client.get_historical_orders_query(
1115
+ res = await self.graphql_client.get_historical_orders_query(
575
1116
  order_ids=order_ids,
576
1117
  venue=venue,
577
1118
  account=account,
@@ -579,54 +1120,41 @@ class AsyncClient:
579
1120
  from_inclusive=from_inclusive,
580
1121
  to_exclusive=to_exclusive,
581
1122
  )
582
- return orders.historical_orders
1123
+ return res.historical_orders
583
1124
 
584
1125
  async def get_order(self, order_id: str) -> Optional[OrderFields]:
585
1126
  """
586
- Returns the OrderFields object for the specified order.
587
- 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.
588
1129
 
589
1130
  Args:
590
1131
  order_id: the order id to get
591
- Returns:
592
- the OrderFields object for the order
593
-
594
- Queries open_orders first then queries historical_orders
595
1132
  """
596
- open_orders = await self.graphql_client.get_open_orders_query(
597
- order_ids=[order_id]
598
- )
599
-
600
- 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:
601
1135
  if open_order.id == order_id:
602
1136
  return open_order
603
1137
 
604
- historical_orders = await self.graphql_client.get_historical_orders_query(
1138
+ res = await self.graphql_client.get_historical_orders_query(
605
1139
  order_ids=[order_id]
606
1140
  )
607
-
608
- if historical_orders.historical_orders:
609
- return historical_orders.historical_orders[0]
1141
+ if res.historical_orders and len(res.historical_orders) > 0:
1142
+ return res.historical_orders[0]
610
1143
 
611
1144
  async def get_orders(self, order_ids: list[str]) -> list[Optional[OrderFields]]:
612
1145
  """
613
- Returns a list of OrderFields objects for the specified orders.
614
- 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.
615
1148
 
616
1149
  Args:
617
1150
  order_ids: a list of order ids to get
618
- Returns:
619
- a list of OrderFields objects for the orders
620
-
621
- Plural form of get_order
622
1151
  """
623
1152
  orders_dict: dict[str, Optional[OrderFields]] = {
624
1153
  order_id: None for order_id in order_ids
625
1154
  }
626
1155
 
627
- open_orders = (
628
- await self.graphql_client.get_open_orders_query(order_ids=order_ids)
629
- ).open_orders
1156
+ res = await self.graphql_client.get_open_orders_query(order_ids=order_ids)
1157
+ open_orders = res.open_orders
630
1158
  for open_order in open_orders:
631
1159
  orders_dict[open_order.id] = open_order
632
1160
 
@@ -634,11 +1162,10 @@ class AsyncClient:
634
1162
  order_id for order_id in order_ids if orders_dict[order_id] is None
635
1163
  ]
636
1164
 
637
- historical_orders = (
638
- await self.graphql_client.get_historical_orders_query(
639
- order_ids=not_open_order_ids
640
- )
641
- ).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
642
1169
  for historical_order in historical_orders:
643
1170
  orders_dict[historical_order.id] = historical_order
644
1171
 
@@ -646,310 +1173,96 @@ class AsyncClient:
646
1173
 
647
1174
  async def get_fills(
648
1175
  self,
649
- from_inclusive: Optional[datetime],
650
- to_exclusive: Optional[datetime],
1176
+ from_inclusive: Optional[datetime] = None,
1177
+ to_exclusive: Optional[datetime] = None,
651
1178
  venue: Optional[str] = None,
652
1179
  account: Optional[str] = None,
653
1180
  order_id: Optional[str] = None,
654
1181
  ) -> GetFillsQueryFolioHistoricalFills:
655
1182
  """
656
- Returns a list of fills for the given filters.
1183
+ Returns all fills matching the given filters.
657
1184
 
658
1185
  Args:
659
1186
  from_inclusive: the start date to get fills for
660
1187
  to_exclusive: the end date to get fills for
661
1188
  venue: the venue to get fills for, e.g. "CME"
662
- account: the account to get fills for,
663
- can be the account id( (a UUID) or the account name (e.g. CQG:00000)
1189
+ account: account uuid or name
664
1190
  order_id: the order id to get fills for
665
- Returns:
666
- a list of GetFillsQueryFolioHistoricalFills
667
1191
  """
668
- fills = await self.graphql_client.get_fills_query(
1192
+ res = await self.graphql_client.get_fills_query(
669
1193
  venue, account, order_id, from_inclusive, to_exclusive
670
1194
  )
671
- return fills.historical_fills
672
-
673
- # ------------------------------------------------------------
674
- # Market Data
675
- # ------------------------------------------------------------
676
-
677
- async def get_market_status(
678
- self, symbol: TradableProduct, venue: str
679
- ) -> MarketStatusFields:
680
- """
681
- Returns market status for symbol (ie if it is quoting and trading).
682
-
683
- Args:
684
- symbol: the symbol to get the market status for, e.g. "ES 20250321 CME Future/USD"
685
- venue: the venue that the symbol is traded at, e.g. CME
686
- Returns:
687
- MarketStatusFields for the symbol
688
- """
689
- market_status = await self.graphql_client.get_market_status_query(symbol, venue)
690
- return market_status.market_status
1195
+ return res.historical_fills
691
1196
 
692
- async def get_market_snapshot(
693
- self, symbol: TradableProduct, venue: str
694
- ) -> MarketTickerFields:
695
- """
696
- This is an alias for l1_book_snapshot.
697
-
698
- Args:
699
- symbol: the symbol to get the market snapshot for, e.g. "ES 20250321 CME Future/USD"
700
- venue: the venue that the symbol is traded at, e.g. CME
701
- Returns:
702
- MarketTickerFields for the symbol
703
- """
704
- return await self.get_l1_book_snapshot(symbol=symbol, venue=venue)
705
-
706
- async def get_market_snapshots(
707
- self, symbols: list[TradableProduct], venue: str
708
- ) -> Sequence[MarketTickerFields]:
709
- """
710
- This is an alias for l1_book_snapshot.
711
-
712
- Args:
713
- symbols: the symbols to get the market snapshots for
714
- venue: the venue that the symbols are traded at
715
- Returns:
716
- a list of MarketTickerFields for the symbols
717
- """
718
- return await self.get_l1_book_snapshots(
719
- venue=venue, symbols=symbols # type: ignore
720
- )
721
-
722
- async def get_historical_candles(
1197
+ async def orderflow(
723
1198
  self,
724
- symbol: str,
725
- candle_width: grpc_definitions.CandleWidth,
726
- start: datetime,
727
- end: datetime,
728
- ) -> HistoricalCandlesResponse:
1199
+ request_iterator: AsyncIterator[OrderflowRequest],
1200
+ ) -> AsyncIterator[Orderflow]:
729
1201
  """
730
- Gets the historical candles for a symbol.
731
-
732
- Args:
733
- symbol: the symbol to get the candles for
734
- venue: the venue of the symbol
735
- candle_width: the width of the candles
736
- start: the start date to get candles for
737
- end: the end date to get candles for
738
- Returns:
739
- a list of CandleFields for the specified candles
740
- """
741
- request = HistoricalCandlesRequest(
742
- symbol=symbol,
743
- candle_width=candle_width,
744
- start_date=start,
745
- end_date=end,
746
- )
1202
+ A two-way channel for both order entry and listening to order updates (fills, acks, outs, etc.).
747
1203
 
748
- return await self.grpc_client.request(request)
1204
+ This is considered the most efficient way to trade in this SDK.
749
1205
 
750
- async def get_l1_book_snapshot(
751
- self,
752
- symbol: str,
753
- venue: str,
754
- ) -> MarketTickerFields:
755
- """
756
- Gets the L1 book snapshot for a symbol.
1206
+ Example:
1207
+ See test_orderflow.py for an example.
757
1208
 
758
- Args:
759
- symbol: the symbol to get the l1 book snapshot for
760
- venue: the venue that the symbol is traded at
761
- Returns:
762
- MarketTickerFields for the symbol
1209
+ This WILL block the event loop until the stream is closed.
763
1210
  """
764
- snapshot = await self.graphql_client.get_l_1_book_snapshot_query(
765
- 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,
766
1217
  )
767
- return snapshot.ticker
768
-
769
- async def get_l1_book_snapshots(
770
- self, symbols: list[str], venue: str
771
- ) -> Sequence[MarketTickerFields]:
772
- """
773
- Gets the L1 book snapshots for a list of symbols.
774
-
775
- Args:
776
- symbols: the symbols to get the l1 book snapshots for
777
- venue: the venue that the symbols are traded at
778
- Returns:
779
- a list of MarketTickerFields for the symbols
780
- """
781
- snapshot = await self.graphql_client.get_l_1_book_snapshots_query(
782
- venue=venue, symbols=symbols
1218
+ call = stub(
1219
+ request_iterator, metadata=(("authorization", f"Bearer {grpc_client.jwt}"),)
783
1220
  )
784
- return snapshot.tickers
785
-
786
- async def get_l2_book_snapshot(self, symbol: str, venue: str) -> L2BookFields:
787
- """
788
- Gets the L2 book snapshot for a symbol.
1221
+ async for update in call:
1222
+ yield update
789
1223
 
790
- Args:
791
- symbol: the symbol to get the l2 book snapshot for
792
- venue: the venue that the symbol is traded at
793
- Returns:
794
- L2BookFields for the symbol
795
-
796
- Note: this does NOT update, it is a snapshot at a given time
797
- For an object that updates, use subscribe_l2_book
798
- """
799
- l2_book = await self.graphql_client.get_l_2_book_snapshot_query(
800
- symbol=symbol, venue=venue
801
- )
802
- return l2_book.l_2_book_snapshot
803
-
804
- async def subscribe_l1_book_stream(
805
- self, symbols: list[TradableProduct], venue: str
806
- ) -> AsyncIterator[L1BookSnapshot]:
807
- """
808
- Subscribe to the stream of L1BookSnapshots for a symbol.
809
-
810
- Args:
811
- symbol: the symbol to subscribe to
812
- venue: the venue to subscribe to
813
- Returns:
814
- an async iterator that yields L1BookSnapshot, representing the l1 book updates
815
- """
816
- async for snapshot in await self.grpc_client.subscribe_l1_books_stream(
817
- symbols=[str(s) for s in symbols]
818
- ):
819
- yield snapshot
820
-
821
- async def subscribe_l2_book_stream(
822
- self, symbol: TradableProduct, venue: str
823
- ) -> AsyncIterator[L2BookUpdate]:
824
- """
825
- Subscribe to the stream of L2BookUpdates for a symbol.
826
-
827
- IMPORTANT: note that the Snapshot is a different type than
828
- L2BookSnapshot
829
- Args:
830
- symbol: the symbol to subscribe to
831
- venue: the venue to subscribe to
832
- Returns:
833
- an async iterator that yields L2BookFields
834
- L2BookFields is either a Snapshot or a Diff
835
- See the grpc_client code for how to handle the different types
836
- """
837
- async for snapshot in self.grpc_client.subscribe_l2_books_stream(
838
- symbol=symbol, venue=venue
839
- ):
840
- yield snapshot
841
-
842
- async def subscribe_l1_book(
843
- self, symbols: list[TradableProduct]
844
- ) -> list[L1BookSnapshot]:
845
- """
846
- Returns a L1BookSnapshot object that is constantly updating in the background.
847
-
848
- Args:
849
- symbols: the symbols to subscribe to
850
- Return:
851
- a list of L1BookSnapshot objects that are constantly updating in the background
852
- For the duration of the program, the client will be subscribed to the stream
853
- and be updating the L1BookSnapshot.
854
-
855
- IMPORTANT: The L1BookSnapshot will be initialized with
856
- a timestamp (field tn and ts) of 0
857
- along with None for bid and ask
858
-
859
- The reference to the object should be kept, but can also be referenced via
860
- client.grpc_client.l1_books.get(symbol)
861
-
862
- If you want direct access to the stream to do on_update type code, you can
863
- call client.grpc_client.stream_l1_books
864
- """
865
- books = self.grpc_client.initialize_l1_books(symbols)
866
- asyncio.create_task(self.grpc_client.watch_l1_books(symbols=symbols))
867
- i = 0
868
- while not all(book.ts > 0 for book in books) and i < 10:
869
- await asyncio.sleep(0.2)
870
- i += 1
871
- if i == 10:
872
- raise ValueError(
873
- f"Could not get L1 books for {symbols}. Check if market is quoting via client.get_market_status."
874
- )
875
-
876
- return books
877
-
878
- async def subscribe_l2_book(
1224
+ async def stream_orderflow(
879
1225
  self,
880
- symbol: TradableProduct,
881
- venue: Optional[str],
882
- ) -> L2BookSnapshot:
1226
+ account: Optional[grpc_definitions.AccountIdOrName] = None,
1227
+ execution_venue: Optional[str] = None,
1228
+ trader: Optional[grpc_definitions.TraderIdOrEmail] = None,
1229
+ ) -> AsyncIterator[Orderflow]:
883
1230
  """
884
- Returns a L2BookSnapshot object that is constantly updating in the background.
1231
+ A stream for listening to order updates (fills, acks, outs, etc.).
885
1232
 
886
- Args:
887
- symbols: the symbols to subscribe to
888
- Return:
889
- a list of L2BookSnapshot object that is constantly updating in the background
890
- For the duration of the program, the client will be subscribed to the stream
891
- and be updating the L2BookSnapshot.
892
-
893
- IMPORTANT: The LBBookSnapshot will be initialized with
894
- a timestamp (field tn and ts) of 0
895
- along with None for bid and ask
896
-
897
- The reference to the object should be kept, but can also be referenced via
898
- client.grpc_client.l1_books.get(symbol)
899
-
900
- If you want direct access to the stream to do on_update type code, you can
901
- call client.grpc_client.stream_l2_book
902
- """
903
- book = self.grpc_client.initialize_l2_book(symbol, venue)
904
- asyncio.create_task(self.grpc_client.watch_l2_book(symbol, venue))
905
- i = 0
906
- while book.ts == 0 and i < 10:
907
- await asyncio.sleep(0.2)
908
- i += 1
909
- if book.ts == 0:
910
- if venue:
911
- market_status = await self.get_market_status(symbol, venue)
912
- if not market_status.is_quoting:
913
- raise ValueError(
914
- f"Market {symbol} is currently closed, cannot get L2."
915
- )
916
- raise ValueError(f"Could not get L2 book for {symbol}.")
917
-
918
- return book
1233
+ Example:
1234
+ ```python
1235
+ request = SubscribeOrderflowRequest.new()
1236
+ async for of in client.subscribe_orderflow_stream(request):
1237
+ print(of)
1238
+ ```
919
1239
 
920
- def subscribe_trades_stream(
921
- self, symbol: TradableProduct, venue: Optional[str]
922
- ) -> AsyncIterator[Trade]:
923
- """
924
- Subscribe to a stream of trades for a symbol.
1240
+ This WILL block the event loop until the stream is closed.
925
1241
  """
926
- request = SubscribeTradesRequest(symbol=symbol, venue=venue)
927
- return self.grpc_client.subscribe(request)
928
-
929
- def subscribe_candles_stream(
930
- self,
931
- symbol: TradableProduct,
932
- venue: Optional[str],
933
- candle_widths: Optional[list[grpc_definitions.CandleWidth]],
934
- ) -> AsyncIterator[Candle]:
935
- """
936
- Subscribe to a stream of candles for a symbol.
937
- """
938
- request = SubscribeCandlesRequest(
939
- symbol=str(symbol),
940
- venue=venue,
941
- 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,
942
1252
  )
943
- 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
944
1256
 
945
1257
  # ------------------------------------------------------------
946
- # Order Entry and Cancellation
1258
+ # Order entry
947
1259
  # ------------------------------------------------------------
948
1260
 
949
- async def send_limit_order(
1261
+ async def place_limit_order(
950
1262
  self,
951
1263
  *,
952
- symbol: TradableProduct,
1264
+ id: Optional[str] = None,
1265
+ symbol: TradableProduct | str,
953
1266
  execution_venue: Optional[str],
954
1267
  odir: OrderDir,
955
1268
  quantity: Decimal,
@@ -1027,7 +1340,7 @@ class AsyncClient:
1027
1340
  quantity,
1028
1341
  order_type,
1029
1342
  time_in_force,
1030
- None,
1343
+ id,
1031
1344
  trader,
1032
1345
  account,
1033
1346
  limit_price,
@@ -1042,7 +1355,8 @@ class AsyncClient:
1042
1355
  async def send_market_pro_order(
1043
1356
  self,
1044
1357
  *,
1045
- symbol: TradableProduct,
1358
+ id: Optional[str] = None,
1359
+ symbol: TradableProduct | str,
1046
1360
  execution_venue: str,
1047
1361
  odir: OrderDir,
1048
1362
  quantity: Decimal,
@@ -1070,36 +1384,33 @@ class AsyncClient:
1070
1384
  If the order is rejected, the order.reject_reason and order.reject_message will be set
1071
1385
  """
1072
1386
 
1073
- # Check for GQL failures
1074
- bbo_snapshot = await self.get_market_snapshot(
1075
- symbol=symbol, venue=execution_venue
1076
- )
1077
- if bbo_snapshot is None:
1387
+ ticker = await self.get_ticker(symbol, execution_venue)
1388
+ if ticker is None:
1078
1389
  raise ValueError(
1079
- 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}"
1080
1391
  )
1081
1392
 
1082
1393
  price_band = price_band_pairs.get(symbol, None)
1083
1394
 
1084
1395
  if odir == OrderDir.BUY:
1085
- if bbo_snapshot.ask_price is None:
1396
+ if ticker.ask_price is None:
1086
1397
  raise ValueError(
1087
1398
  f"Failed to send market order with reason: no ask price for {symbol}"
1088
1399
  )
1089
- limit_price = bbo_snapshot.ask_price * (1 + fraction_through_market)
1400
+ limit_price = ticker.ask_price * (1 + fraction_through_market)
1090
1401
 
1091
- if price_band and bbo_snapshot.last_price:
1092
- 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
1093
1404
  limit_price = min(limit_price, price_band_reference_price)
1094
1405
 
1095
1406
  else:
1096
- if bbo_snapshot.bid_price is None:
1407
+ if ticker.bid_price is None:
1097
1408
  raise ValueError(
1098
1409
  f"Failed to send market order with reason: no bid price for {symbol}"
1099
1410
  )
1100
- limit_price = bbo_snapshot.bid_price * (1 - fraction_through_market)
1101
- if price_band and bbo_snapshot.last_price:
1102
- 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
1103
1414
  limit_price = min(limit_price, price_band_reference_price)
1104
1415
 
1105
1416
  # Conservatively round price to nearest tick
@@ -1117,7 +1428,8 @@ class AsyncClient:
1117
1428
  ):
1118
1429
  limit_price = tick_round_method(limit_price, tick_size)
1119
1430
 
1120
- return await self.send_limit_order(
1431
+ return await self.place_limit_order(
1432
+ id=id,
1121
1433
  symbol=symbol,
1122
1434
  execution_venue=execution_venue,
1123
1435
  odir=odir,