architect-py 3.2.1__py3-none-any.whl → 5.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. architect_py/__init__.py +18 -2
  2. architect_py/async_client.py +1089 -658
  3. architect_py/client.py +36 -33
  4. architect_py/client_interface.py +63 -0
  5. architect_py/common_types/__init__.py +6 -0
  6. architect_py/common_types/order_dir.py +91 -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/__init__.py +2 -0
  10. architect_py/graphql_client/client.py +3 -6
  11. architect_py/graphql_client/enums.py +5 -0
  12. architect_py/graphql_client/fragments.py +3 -6
  13. architect_py/graphql_client/get_fills_query.py +2 -1
  14. architect_py/graphql_client/search_symbols_query.py +2 -1
  15. architect_py/graphql_client/subscribe_orderflow.py +2 -1
  16. architect_py/graphql_client/subscribe_trades.py +2 -1
  17. architect_py/grpc/__init__.py +145 -0
  18. architect_py/grpc/client.py +94 -0
  19. architect_py/{grpc_client → grpc/models}/Accounts/AccountsRequest.py +6 -3
  20. architect_py/{grpc_client → grpc/models}/Accounts/AccountsResponse.py +1 -1
  21. architect_py/{grpc_client → grpc/models}/Accounts/__init__.py +1 -1
  22. architect_py/grpc/models/Algo/AlgoOrder.py +114 -0
  23. architect_py/grpc/models/Algo/AlgoOrderRequest.py +46 -0
  24. architect_py/grpc/models/Algo/AlgoOrdersRequest.py +72 -0
  25. architect_py/grpc/models/Algo/AlgoOrdersResponse.py +27 -0
  26. architect_py/grpc/models/Algo/CreateAlgoOrderRequest.py +56 -0
  27. architect_py/grpc/models/Algo/PauseAlgoRequest.py +42 -0
  28. architect_py/grpc/models/Algo/PauseAlgoResponse.py +20 -0
  29. architect_py/grpc/models/Algo/StartAlgoRequest.py +42 -0
  30. architect_py/grpc/models/Algo/StartAlgoResponse.py +20 -0
  31. architect_py/grpc/models/Algo/StopAlgoRequest.py +42 -0
  32. architect_py/grpc/models/Algo/StopAlgoResponse.py +20 -0
  33. architect_py/{grpc_client → grpc/models}/Algo/__init__.py +1 -1
  34. architect_py/grpc/models/Auth/CreateJwtRequest.py +47 -0
  35. architect_py/grpc/models/Auth/CreateJwtResponse.py +23 -0
  36. architect_py/{grpc_client/Cpty → grpc/models/Auth}/__init__.py +1 -1
  37. architect_py/grpc/models/Boss/DepositsRequest.py +40 -0
  38. architect_py/grpc/models/Boss/DepositsResponse.py +27 -0
  39. architect_py/grpc/models/Boss/RqdAccountStatisticsRequest.py +42 -0
  40. architect_py/grpc/models/Boss/RqdAccountStatisticsResponse.py +25 -0
  41. architect_py/grpc/models/Boss/StatementUrlRequest.py +40 -0
  42. architect_py/grpc/models/Boss/StatementUrlResponse.py +23 -0
  43. architect_py/grpc/models/Boss/StatementsRequest.py +40 -0
  44. architect_py/grpc/models/Boss/StatementsResponse.py +27 -0
  45. architect_py/grpc/models/Boss/WithdrawalsRequest.py +40 -0
  46. architect_py/grpc/models/Boss/WithdrawalsResponse.py +27 -0
  47. architect_py/{grpc_client/Folio → grpc/models/Boss}/__init__.py +1 -1
  48. architect_py/grpc/models/Core/ConfigRequest.py +37 -0
  49. architect_py/grpc/models/Core/ConfigResponse.py +25 -0
  50. architect_py/grpc/models/Core/__init__.py +2 -0
  51. architect_py/{grpc_client → grpc/models}/Cpty/CptyRequest.py +5 -4
  52. architect_py/{grpc_client → grpc/models}/Cpty/CptyResponse.py +6 -6
  53. architect_py/grpc/models/Cpty/CptyStatus.py +48 -0
  54. architect_py/grpc/models/Cpty/CptyStatusRequest.py +45 -0
  55. architect_py/grpc/models/Cpty/CptysRequest.py +37 -0
  56. architect_py/grpc/models/Cpty/CptysResponse.py +27 -0
  57. architect_py/grpc/models/Cpty/__init__.py +2 -0
  58. architect_py/{grpc_client → grpc/models}/Folio/AccountHistoryRequest.py +2 -2
  59. architect_py/{grpc_client → grpc/models}/Folio/AccountHistoryResponse.py +1 -1
  60. architect_py/{grpc_client → grpc/models}/Folio/AccountSummariesRequest.py +2 -2
  61. architect_py/{grpc_client → grpc/models}/Folio/AccountSummariesResponse.py +1 -1
  62. architect_py/{grpc_client → grpc/models}/Folio/AccountSummary.py +1 -1
  63. architect_py/{grpc_client → grpc/models}/Folio/AccountSummaryRequest.py +2 -2
  64. architect_py/{grpc_client → grpc/models}/Folio/HistoricalFillsRequest.py +7 -4
  65. architect_py/{grpc_client → grpc/models}/Folio/HistoricalFillsResponse.py +1 -1
  66. architect_py/{grpc_client → grpc/models}/Folio/HistoricalOrdersRequest.py +3 -3
  67. architect_py/{grpc_client → grpc/models}/Folio/HistoricalOrdersResponse.py +1 -1
  68. architect_py/grpc/models/Folio/__init__.py +2 -0
  69. architect_py/{grpc_client → grpc/models}/Health/HealthCheckRequest.py +2 -2
  70. architect_py/{grpc_client → grpc/models}/Health/HealthCheckResponse.py +1 -1
  71. architect_py/grpc/models/Health/__init__.py +2 -0
  72. architect_py/{grpc_client → grpc/models}/Marketdata/Candle.py +1 -1
  73. architect_py/{grpc_client → grpc/models}/Marketdata/HistoricalCandlesRequest.py +11 -8
  74. architect_py/{grpc_client → grpc/models}/Marketdata/HistoricalCandlesResponse.py +1 -1
  75. architect_py/{grpc_client → grpc/models}/Marketdata/L1BookSnapshot.py +52 -5
  76. architect_py/{grpc_client → grpc/models}/Marketdata/L1BookSnapshotRequest.py +8 -3
  77. architect_py/{grpc_client → grpc/models}/Marketdata/L1BookSnapshotsRequest.py +6 -3
  78. architect_py/{grpc_client → grpc/models}/Marketdata/L2BookSnapshot.py +1 -1
  79. architect_py/{grpc_client → grpc/models}/Marketdata/L2BookSnapshotRequest.py +2 -2
  80. architect_py/{grpc_client → grpc/models}/Marketdata/Liquidation.py +2 -2
  81. architect_py/{grpc_client → grpc/models}/Marketdata/MarketStatus.py +1 -1
  82. architect_py/{grpc_client → grpc/models}/Marketdata/MarketStatusRequest.py +2 -2
  83. architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeCandlesRequest.py +2 -2
  84. architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeCurrentCandlesRequest.py +3 -4
  85. architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeL1BookSnapshotsRequest.py +6 -3
  86. architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeL2BookUpdatesRequest.py +2 -2
  87. architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeLiquidationsRequest.py +2 -2
  88. architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeManyCandlesRequest.py +2 -2
  89. architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeTickersRequest.py +2 -2
  90. architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeTradesRequest.py +2 -2
  91. architect_py/{grpc_client → grpc/models}/Marketdata/Ticker.py +14 -3
  92. architect_py/{grpc_client → grpc/models}/Marketdata/TickerRequest.py +2 -2
  93. architect_py/{grpc_client → grpc/models}/Marketdata/TickersRequest.py +2 -2
  94. architect_py/{grpc_client → grpc/models}/Marketdata/TickersResponse.py +1 -1
  95. architect_py/{grpc_client → grpc/models}/Marketdata/Trade.py +2 -2
  96. architect_py/grpc/models/Marketdata/__init__.py +2 -0
  97. architect_py/grpc/models/Oms/Cancel.py +90 -0
  98. architect_py/{grpc_client → grpc/models}/Oms/CancelAllOrdersRequest.py +2 -2
  99. architect_py/{grpc_client → grpc/models}/Oms/CancelAllOrdersResponse.py +1 -1
  100. architect_py/{grpc_client → grpc/models}/Oms/CancelOrderRequest.py +2 -2
  101. architect_py/{grpc_client → grpc/models}/Oms/OpenOrdersRequest.py +2 -2
  102. architect_py/{grpc_client → grpc/models}/Oms/OpenOrdersResponse.py +1 -1
  103. architect_py/{grpc_client → grpc/models}/Oms/Order.py +6 -13
  104. architect_py/{grpc_client → grpc/models}/Oms/PendingCancelsRequest.py +2 -2
  105. architect_py/{grpc_client → grpc/models}/Oms/PendingCancelsResponse.py +1 -1
  106. architect_py/{grpc_client → grpc/models}/Oms/PlaceOrderRequest.py +16 -23
  107. architect_py/grpc/models/Oms/__init__.py +2 -0
  108. architect_py/grpc/models/OptionsMarketdata/OptionsChain.py +30 -0
  109. architect_py/grpc/models/OptionsMarketdata/OptionsChainGreeks.py +30 -0
  110. architect_py/grpc/models/OptionsMarketdata/OptionsChainGreeksRequest.py +47 -0
  111. architect_py/grpc/models/OptionsMarketdata/OptionsChainRequest.py +45 -0
  112. architect_py/grpc/models/OptionsMarketdata/OptionsExpirations.py +29 -0
  113. architect_py/grpc/models/OptionsMarketdata/OptionsExpirationsRequest.py +42 -0
  114. architect_py/grpc/models/OptionsMarketdata/__init__.py +2 -0
  115. architect_py/{grpc_client → grpc/models}/Orderflow/DropcopyRequest.py +2 -2
  116. architect_py/{grpc_client → grpc/models}/Orderflow/OrderflowRequest.py +2 -1
  117. architect_py/{grpc_client → grpc/models}/Orderflow/SubscribeOrderflowRequest.py +2 -2
  118. architect_py/grpc/models/Orderflow/__init__.py +2 -0
  119. architect_py/grpc/models/Symbology/DownloadProductCatalogRequest.py +42 -0
  120. architect_py/grpc/models/Symbology/DownloadProductCatalogResponse.py +27 -0
  121. architect_py/grpc/models/Symbology/ExecutionInfoRequest.py +47 -0
  122. architect_py/grpc/models/Symbology/ExecutionInfoResponse.py +27 -0
  123. architect_py/{grpc_client → grpc/models}/Symbology/PruneExpiredSymbolsRequest.py +2 -2
  124. architect_py/{grpc_client → grpc/models}/Symbology/PruneExpiredSymbolsResponse.py +1 -1
  125. architect_py/{grpc_client → grpc/models}/Symbology/SubscribeSymbology.py +1 -1
  126. architect_py/{grpc_client → grpc/models}/Symbology/SymbologyRequest.py +2 -2
  127. architect_py/{grpc_client → grpc/models}/Symbology/SymbologySnapshot.py +7 -2
  128. architect_py/{grpc_client → grpc/models}/Symbology/SymbologyUpdate.py +9 -2
  129. architect_py/{grpc_client → grpc/models}/Symbology/SymbolsRequest.py +2 -2
  130. architect_py/{grpc_client → grpc/models}/Symbology/SymbolsResponse.py +1 -1
  131. architect_py/grpc/models/Symbology/UploadProductCatalogRequest.py +49 -0
  132. architect_py/grpc/models/Symbology/UploadProductCatalogResponse.py +20 -0
  133. architect_py/{grpc_client → grpc/models}/Symbology/UploadSymbologyRequest.py +2 -2
  134. architect_py/{grpc_client → grpc/models}/Symbology/UploadSymbologyResponse.py +1 -1
  135. architect_py/grpc/models/Symbology/__init__.py +2 -0
  136. architect_py/grpc/models/__init__.py +2 -0
  137. architect_py/{grpc_client → grpc/models}/definitions.py +671 -934
  138. architect_py/grpc/resolve_endpoint.py +70 -0
  139. architect_py/{grpc_client/grpc_server.py → grpc/server.py} +13 -9
  140. architect_py/grpc/utils.py +32 -0
  141. architect_py/internal_utils/__init__.py +0 -0
  142. architect_py/internal_utils/no_pandas.py +3 -0
  143. architect_py/tests/conftest.py +91 -85
  144. architect_py/tests/test_book_building.py +49 -50
  145. architect_py/tests/test_marketdata.py +168 -0
  146. architect_py/tests/test_order_entry.py +37 -0
  147. architect_py/tests/test_orderflow.py +41 -0
  148. architect_py/tests/test_portfolio_management.py +23 -0
  149. architect_py/tests/test_rounding.py +28 -28
  150. architect_py/tests/test_symbology.py +37 -30
  151. architect_py/utils/nearest_tick.py +2 -5
  152. architect_py/utils/nearest_tick_2.py +1 -2
  153. architect_py/utils/orderbook.py +35 -0
  154. architect_py/utils/pandas.py +44 -0
  155. architect_py/utils/price_bands.py +0 -3
  156. architect_py/utils/symbol_parsing.py +29 -0
  157. architect_py-5.0.0.dist-info/METADATA +54 -0
  158. architect_py-5.0.0.dist-info/RECORD +214 -0
  159. {architect_py-3.2.1.dist-info → architect_py-5.0.0.dist-info}/WHEEL +2 -1
  160. architect_py-5.0.0.dist-info/top_level.txt +4 -0
  161. examples/__init__.py +0 -0
  162. examples/book_subscription.py +52 -0
  163. examples/candles.py +30 -0
  164. examples/common.py +116 -0
  165. examples/external_cpty.py +77 -0
  166. examples/funding_rate_mean_reversion_algo.py +186 -0
  167. examples/order_sending.py +91 -0
  168. examples/stream_l1_marketdata.py +25 -0
  169. examples/stream_l2_marketdata.py +38 -0
  170. examples/trades.py +21 -0
  171. examples/tutorial_async.py +86 -0
  172. examples/tutorial_sync.py +95 -0
  173. scripts/generate_functions_md.py +166 -0
  174. scripts/generate_sync_interface.py +226 -0
  175. scripts/postprocess_grpc.py +604 -0
  176. scripts/preprocess_grpc_schema.py +708 -0
  177. templates/exceptions.py +83 -0
  178. templates/juniper_base_client.py +371 -0
  179. architect_py/client_protocol.py +0 -52
  180. architect_py/grpc_client/Algo/AlgoOrderForTwapAlgo.py +0 -61
  181. architect_py/grpc_client/Algo/CreateAlgoOrderRequestForTwapAlgo.py +0 -59
  182. architect_py/grpc_client/Algo/ModifyAlgoOrderRequestForTwapAlgo.py +0 -45
  183. architect_py/grpc_client/Folio/AggregatedAccountSummariesRequest.py +0 -59
  184. architect_py/grpc_client/Folio/AggregatedAccountSummariesResponse.py +0 -27
  185. architect_py/grpc_client/Health/__init__.py +0 -2
  186. architect_py/grpc_client/Marketdata/__init__.py +0 -2
  187. architect_py/grpc_client/Oms/Cancel.py +0 -42
  188. architect_py/grpc_client/Oms/__init__.py +0 -2
  189. architect_py/grpc_client/Orderflow/__init__.py +0 -2
  190. architect_py/grpc_client/Symbology/__init__.py +0 -2
  191. architect_py/grpc_client/__init__.py +0 -2
  192. architect_py/grpc_client/grpc_client.py +0 -405
  193. architect_py/scalars.py +0 -172
  194. architect_py/tests/test_accounts.py +0 -31
  195. architect_py/tests/test_client.py +0 -29
  196. architect_py/tests/test_grpc_client.py +0 -30
  197. architect_py/tests/test_order_sending.py +0 -61
  198. architect_py/tests/test_snapshots.py +0 -52
  199. architect_py/tests/test_subscriptions.py +0 -129
  200. architect_py-3.2.1.dist-info/METADATA +0 -212
  201. architect_py-3.2.1.dist-info/RECORD +0 -146
  202. /architect_py/{grpc_client → grpc/models}/Marketdata/ArrayOfL1BookSnapshot.py +0 -0
  203. /architect_py/{grpc_client → grpc/models}/Marketdata/L2BookUpdate.py +0 -0
  204. /architect_py/{grpc_client → grpc/models}/Marketdata/TickerUpdate.py +0 -0
  205. /architect_py/{grpc_client → grpc/models}/Orderflow/Dropcopy.py +0 -0
  206. /architect_py/{grpc_client → grpc/models}/Orderflow/Orderflow.py +0 -0
  207. {architect_py-3.2.1.dist-info → architect_py-5.0.0.dist-info/licenses}/LICENSE +0 -0
@@ -1,59 +0,0 @@
1
- # generated by datamodel-codegen:
2
- # filename: Folio/AggregatedAccountSummariesRequest.json
3
-
4
- from __future__ import annotations
5
- from architect_py.grpc_client.Folio.AggregatedAccountSummariesResponse import (
6
- AggregatedAccountSummariesResponse,
7
- )
8
-
9
- from typing import Annotated, List, Optional
10
-
11
- from msgspec import Meta, Struct
12
-
13
- from .. import definitions
14
-
15
-
16
- class AggregatedAccountSummariesRequest(Struct, omit_defaults=True):
17
- accounts: Optional[
18
- Annotated[
19
- List[definitions.AccountIdOrName],
20
- Meta(
21
- description="If trader and accounts are both None, return all accounts for the user"
22
- ),
23
- ]
24
- ] = None
25
- """
26
- If trader and accounts are both None, return all accounts for the user
27
- """
28
- trader: Optional[definitions.TraderIdOrEmail] = None
29
-
30
- # below is a constructor that takes all field titles as arguments for convenience
31
- @classmethod
32
- def new(
33
- cls,
34
- accounts: Optional[List[definitions.AccountIdOrName]] = None,
35
- trader: Optional[definitions.TraderIdOrEmail] = None,
36
- ):
37
- return cls(
38
- accounts,
39
- trader,
40
- )
41
-
42
- def __str__(self) -> str:
43
- return f"AggregatedAccountSummariesRequest(accounts={self.accounts},trader={self.trader})"
44
-
45
- @staticmethod
46
- def get_response_type():
47
- return AggregatedAccountSummariesResponse
48
-
49
- @staticmethod
50
- def get_unannotated_response_type():
51
- return AggregatedAccountSummariesResponse
52
-
53
- @staticmethod
54
- def get_route() -> str:
55
- return "/json.architect.Folio/AggregatedAccountSummaries"
56
-
57
- @staticmethod
58
- def get_rpc_method():
59
- return "unary"
@@ -1,27 +0,0 @@
1
- # generated by datamodel-codegen:
2
- # filename: Folio/AggregatedAccountSummariesResponse.json
3
-
4
- from __future__ import annotations
5
-
6
- from typing import List
7
-
8
- from msgspec import Struct
9
-
10
- from .. import definitions
11
-
12
-
13
- class AggregatedAccountSummariesResponse(Struct, omit_defaults=True):
14
- account_summaries: List[definitions.AggregatedAccountSummary]
15
-
16
- # below is a constructor that takes all field titles as arguments for convenience
17
- @classmethod
18
- def new(
19
- cls,
20
- account_summaries: List[definitions.AggregatedAccountSummary],
21
- ):
22
- return cls(
23
- account_summaries,
24
- )
25
-
26
- def __str__(self) -> str:
27
- return f"AggregatedAccountSummariesResponse(account_summaries={self.account_summaries})"
@@ -1,2 +0,0 @@
1
- # generated by datamodel-codegen:
2
- # filename: processed_schemas
@@ -1,2 +0,0 @@
1
- # generated by datamodel-codegen:
2
- # filename: processed_schemas
@@ -1,42 +0,0 @@
1
- # generated by datamodel-codegen:
2
- # filename: Oms/Cancel.json
3
-
4
- from __future__ import annotations
5
-
6
- from typing import Annotated, Optional
7
-
8
- from msgspec import Meta, Struct
9
-
10
- from .. import definitions
11
-
12
-
13
- class Cancel(Struct, omit_defaults=True):
14
- id: definitions.OrderId
15
- o: definitions.CancelStatus
16
- tn: Annotated[int, Meta(ge=0)]
17
- ts: int
18
- xid: str
19
- r: Optional[str] = None
20
-
21
- # below is a constructor that takes all field titles as arguments for convenience
22
- @classmethod
23
- def new(
24
- cls,
25
- id: definitions.OrderId,
26
- o: definitions.CancelStatus,
27
- tn: int,
28
- ts: int,
29
- xid: str,
30
- r: Optional[str] = None,
31
- ):
32
- return cls(
33
- id,
34
- o,
35
- tn,
36
- ts,
37
- xid,
38
- r,
39
- )
40
-
41
- def __str__(self) -> str:
42
- return f"Cancel(id={self.id},o={self.o},tn={self.tn},ts={self.ts},xid={self.xid},r={self.r})"
@@ -1,2 +0,0 @@
1
- # generated by datamodel-codegen:
2
- # filename: processed_schemas
@@ -1,2 +0,0 @@
1
- # generated by datamodel-codegen:
2
- # filename: processed_schemas
@@ -1,2 +0,0 @@
1
- # generated by datamodel-codegen:
2
- # filename: processed_schemas
@@ -1,2 +0,0 @@
1
- # datamodel-code-generator stomps on the __init__.py file so we import from adjacent file
2
- from .grpc_client import *
@@ -1,405 +0,0 @@
1
- from asyncio.log import logger
2
- from types import UnionType
3
- from typing import (
4
- Any,
5
- AsyncIterator,
6
- Optional,
7
- Protocol,
8
- Type,
9
- TypeVar,
10
- cast,
11
- )
12
- from datetime import datetime, timedelta
13
- from urllib.parse import urlparse
14
-
15
- import msgspec
16
-
17
- import dns.asyncresolver
18
- import dns.resolver
19
- from dns.rdtypes.IN.SRV import SRV
20
-
21
- import grpc
22
-
23
- import bisect
24
- from decimal import Decimal
25
-
26
- from architect_py.graphql_client.client import GraphQLClient
27
-
28
-
29
- from architect_py.grpc_client.Marketdata.L1BookSnapshot import L1BookSnapshot
30
- from architect_py.grpc_client.Marketdata.L1BookSnapshotRequest import (
31
- L1BookSnapshotRequest,
32
- )
33
- from architect_py.grpc_client.Marketdata.L2BookSnapshot import L2BookSnapshot
34
- from architect_py.grpc_client.Marketdata.L2BookSnapshotRequest import (
35
- L2BookSnapshotRequest,
36
- )
37
- from architect_py.grpc_client.Marketdata.L2BookUpdate import (
38
- L2BookUpdate,
39
- )
40
- from architect_py.grpc_client.Marketdata.SubscribeL1BookSnapshotsRequest import (
41
- SubscribeL1BookSnapshotsRequest,
42
- )
43
- from architect_py.grpc_client.Marketdata.SubscribeL2BookUpdatesRequest import (
44
- SubscribeL2BookUpdatesRequest,
45
- )
46
- from architect_py.grpc_client.Orderflow.Orderflow import Orderflow
47
- from architect_py.grpc_client.Orderflow.OrderflowRequest import (
48
- OrderflowRequest,
49
- OrderflowRequest_route,
50
- OrderflowRequestUnannotatedResponseType,
51
- )
52
- from architect_py.grpc_client.definitions import L2BookDiff
53
- from architect_py.scalars import TradableProduct
54
-
55
-
56
- """
57
- get_account_summaries_for_cpty
58
- """
59
-
60
-
61
- def enc_hook(obj: Any) -> Any:
62
- if isinstance(obj, TradableProduct):
63
- return str(obj)
64
-
65
-
66
- encoder = msgspec.json.Encoder(enc_hook=enc_hook)
67
- ResponseTypeGeneric = TypeVar("ResponseTypeGeneric", covariant=True)
68
-
69
-
70
- class RequestType(Protocol[ResponseTypeGeneric]):
71
- @staticmethod
72
- def get_unannotated_response_type() -> Type[ResponseTypeGeneric]: ...
73
-
74
- @staticmethod
75
- def get_response_type() -> Type[ResponseTypeGeneric]: ...
76
-
77
- @staticmethod
78
- def get_route() -> str: ...
79
-
80
- @staticmethod
81
- def get_rpc_method() -> Any: ...
82
-
83
-
84
- class GRPCClient:
85
- jwt: str
86
- jwt_expiration: datetime
87
-
88
- graphql_client: GraphQLClient
89
- l1_books: dict[TradableProduct, L1BookSnapshot]
90
- l2_books: dict[TradableProduct, L2BookSnapshot]
91
- channel: grpc.aio.Channel
92
- _decoders: dict[type | UnionType, msgspec.json.Decoder]
93
-
94
- def __init__(
95
- self,
96
- graphql_client: GraphQLClient,
97
- endpoint: str = "cme.marketdata.architect.co",
98
- ):
99
- """
100
- Please ensure to call the initialize method before using the gRPC client.
101
-
102
- grpc_client = GRPCClient(graphql_client, endpoint)
103
- await grpc_client.initialize()
104
-
105
-
106
- Brave users may create their own requests using the subscribe and request methods.
107
- The types are correct so if a typechecker such as PyLance is throwing errors,
108
- it's likely a bug in user code.
109
-
110
- async for snap in self.subscribe(
111
- RequestType.get_request_helper(), # add args/kwargs here
112
- ):
113
-
114
- snap = await self.request(
115
- RequestType.get_request_helper(), # add args/kwargs here
116
- )
117
- """
118
- self.graphql_client = graphql_client
119
-
120
- self.jwt_expiration = datetime(1995, 11, 10)
121
-
122
- self.l1_books: dict[TradableProduct, L1BookSnapshot] = {}
123
- self.l2_books: dict[TradableProduct, L2BookSnapshot] = {}
124
- self.endpoint = endpoint
125
-
126
- self._decoders: dict[type | UnionType, msgspec.json.Decoder] = {}
127
-
128
- async def initialize(self) -> Optional[str]:
129
- """
130
- Initialize the gRPC channel with the given endpoint.
131
- Must call this method before using the gRPC client.
132
- """
133
- # "binance-futures-usd-m.marketdata.architect.co",
134
- # "https://usdm.binance.marketdata.architect.co"
135
- # "bybit.marketdata.architect.co",
136
- # "binance.marketdata.architect.co",
137
- # "cme.marketdata.architect.co",
138
- self.channel = await self.get_grpc_channel(self.endpoint)
139
-
140
- async def change_channel(self, endpoint: str) -> None:
141
- self.channel = await self.get_grpc_channel(endpoint)
142
-
143
- async def get_grpc_channel(
144
- self,
145
- endpoint: str,
146
- ) -> grpc.aio.Channel:
147
- if "://" not in endpoint:
148
- endpoint = f"http://{endpoint}"
149
- url = urlparse(endpoint)
150
- if url.hostname is None:
151
- raise Exception(f"Invalid endpoint: {endpoint}")
152
-
153
- is_https = url.scheme == "https"
154
- srv_records: dns.resolver.Answer = await dns.asyncresolver.resolve(
155
- url.hostname, "SRV"
156
- )
157
- if len(srv_records) == 0:
158
- raise Exception(f"No SRV records found for {url.hostname}")
159
-
160
- record = cast(SRV, srv_records[0])
161
-
162
- connect_str = f"{record.target}:{record.port}"
163
- if is_https:
164
- credentials = grpc.ssl_channel_credentials()
165
- return grpc.aio.secure_channel(connect_str, credentials)
166
- else:
167
- return grpc.aio.insecure_channel(connect_str)
168
-
169
- async def refresh_grpc_credentials(self, force: bool = False) -> str:
170
- """
171
- Refresh the JWT for the gRPC channel if it's nearing expiration (within 1 minute).
172
- If force is True, refresh the JWT unconditionally.
173
- """
174
- if force or datetime.now() > self.jwt_expiration - timedelta(minutes=1):
175
- try:
176
- self.jwt = (await self.graphql_client.create_jwt()).create_jwt
177
- self.jwt_expiration = datetime.now() + timedelta(hours=23)
178
- except Exception as e:
179
- logger.error("Failed to refresh gRPC credentials: %s", e)
180
- return self.jwt
181
-
182
- def get_decoder(
183
- self,
184
- response_type: type[ResponseTypeGeneric] | UnionType,
185
- ) -> msgspec.json.Decoder:
186
- try:
187
- return self._decoders[response_type]
188
- except KeyError:
189
- # we use a try / except because we sacrifice first time query
190
- # to optimize for repeated lookups
191
- decoder = msgspec.json.Decoder(type=response_type)
192
- self._decoders[response_type] = decoder
193
- return decoder
194
-
195
- async def request_l1_book_snapshot(self, symbol: TradableProduct) -> L1BookSnapshot:
196
- request = L1BookSnapshotRequest(symbol=symbol)
197
- return await self.request(request)
198
-
199
- async def request_l2_book_snapshot(
200
- self, venue: Optional[str], symbol: TradableProduct
201
- ) -> L2BookSnapshot:
202
- request = L2BookSnapshotRequest(venue=venue, symbol=symbol)
203
- return await self.request(request)
204
-
205
- def initialize_l1_books(
206
- self, symbols: list[TradableProduct]
207
- ) -> list[L1BookSnapshot]:
208
- if symbols is not None:
209
- if len(self.l1_books) + len(symbols) > 100:
210
- raise ValueError(
211
- "Not suggestible to watch more than 100 L1 symbols at once, as it may cause performance issues."
212
- )
213
- symbols = [symbol for symbol in symbols if symbol not in self.l1_books]
214
-
215
- for symbol in symbols:
216
- self.l1_books[symbol] = L1BookSnapshot(symbol, 0, 0)
217
- else:
218
- raise ValueError("symbols must be a list of TradableProduct")
219
- # could technically be None, but we don't want to allow that
220
- # as users should be explicit about what they want to watch
221
-
222
- return [self.l1_books[symbol] for symbol in symbols]
223
-
224
- async def watch_l1_books(self, symbols: list[TradableProduct]) -> None:
225
- symbols_cast = cast(list[str], symbols)
226
-
227
- request = SubscribeL1BookSnapshotsRequest(symbols=symbols_cast)
228
-
229
- async for snap in self.subscribe(request):
230
- book = self.l1_books[cast(TradableProduct, snap.symbol)]
231
- update_struct(book, snap)
232
-
233
- def initialize_l2_book(
234
- self, symbol: TradableProduct, venue: Optional[str]
235
- ) -> L2BookSnapshot:
236
- if symbol not in self.l2_books:
237
- if len(self.l2_books) > 20:
238
- raise ValueError(
239
- "Not suggestible to watch more than 20 L2 symbols at once, as it may cause performance issues."
240
- )
241
- self.l2_books[symbol] = L2BookSnapshot([], [], 0, 0, 0, 0)
242
- return self.l2_books[symbol]
243
-
244
- async def subscribe_l1_books_stream(
245
- self, symbols: list[str]
246
- ) -> AsyncIterator[L1BookSnapshot]:
247
- request = SubscribeL1BookSnapshotsRequest(symbols=symbols)
248
- return self.subscribe(
249
- request,
250
- )
251
-
252
- async def subscribe_l2_books_stream(
253
- self, symbol: TradableProduct, venue: Optional[str]
254
- ) -> AsyncIterator[L2BookUpdate]:
255
- decoder: msgspec.json.Decoder[L2BookUpdate] = self.get_decoder(
256
- SubscribeL2BookUpdatesRequest.get_unannotated_response_type()
257
- )
258
- stub = self.channel.unary_stream(
259
- SubscribeL2BookUpdatesRequest.get_route(),
260
- request_serializer=encoder.encode,
261
- response_deserializer=decoder.decode,
262
- )
263
- req = SubscribeL2BookUpdatesRequest(symbol=symbol, venue=venue)
264
- jwt = await self.refresh_grpc_credentials()
265
- call = stub(req, metadata=(("authorization", f"Bearer {jwt}"),))
266
- async for update in call:
267
- yield update
268
-
269
- async def watch_l2_book(
270
- self, symbol: TradableProduct, venue: Optional[str]
271
- ) -> None:
272
- async for up in self.subscribe_l2_books_stream(symbol, venue):
273
- if isinstance(up, L2BookDiff): # elif up.t = "d": # diff
274
- if symbol not in self.l2_books:
275
- raise ValueError(
276
- f"received update before snapshot for L2 book {symbol}"
277
- )
278
- book = self.l2_books[symbol]
279
- if (
280
- up.sequence_id != book.sequence_id
281
- or up.sequence_number != book.sequence_number + 1
282
- ):
283
- raise ValueError(
284
- f"received update out of order for L2 book {symbol}"
285
- )
286
- L2_update_from_diff(book, up)
287
- elif isinstance(up, L2BookSnapshot): # if up.t = "s":
288
- book = self.l2_books[symbol]
289
- update_struct(book, up)
290
-
291
- async def subscribe_orderflow_stream(
292
- self, request_iterator: AsyncIterator[OrderflowRequest]
293
- ) -> AsyncIterator[Orderflow]:
294
- """
295
- subscribe_orderflow_stream is a duplex_stream meaning that it is a stream that can be read from and written to.
296
- This is a stream that will be used to send orders to the Architect and receive order updates from the Architect.
297
- """
298
- decoder = self.get_decoder(OrderflowRequestUnannotatedResponseType)
299
- stub = self.channel.stream_stream(
300
- OrderflowRequest_route,
301
- request_serializer=encoder.encode,
302
- response_deserializer=decoder.decode,
303
- )
304
- jwt = await self.refresh_grpc_credentials()
305
- call = stub(request_iterator, metadata=(("authorization", f"Bearer {jwt}"),))
306
- async for update in call:
307
- yield update
308
-
309
- async def subscribe(
310
- self,
311
- request: RequestType[ResponseTypeGeneric],
312
- ) -> AsyncIterator[ResponseTypeGeneric]:
313
- """
314
- Generic function for subscribing to a stream of updates from the gRPC server.
315
-
316
- request_type and ResponseTypeGeneric *cannot* be union types
317
- """
318
- decoder: msgspec.json.Decoder[ResponseTypeGeneric] = self.get_decoder(
319
- request.get_unannotated_response_type()
320
- )
321
- stub = self.channel.unary_stream(
322
- request.get_route(),
323
- request_serializer=encoder.encode,
324
- response_deserializer=decoder.decode,
325
- )
326
- jwt = await self.refresh_grpc_credentials()
327
- call = stub(request, metadata=(("authorization", f"Bearer {jwt}"),))
328
- async for update in call:
329
- yield update
330
-
331
- async def request(
332
- self,
333
- request: RequestType[ResponseTypeGeneric],
334
- ) -> ResponseTypeGeneric:
335
- """
336
- Generic function for making a unary request to the gRPC server.
337
-
338
- request_type and ResponseTypeGeneric *cannot* be union types
339
- """
340
- decoder: msgspec.json.Decoder[ResponseTypeGeneric] = self.get_decoder(
341
- request.get_unannotated_response_type()
342
- )
343
- stub = self.channel.unary_unary(
344
- request.get_route(),
345
- request_serializer=encoder.encode,
346
- response_deserializer=decoder.decode,
347
- )
348
- jwt = await self.refresh_grpc_credentials()
349
- return await stub(request, metadata=(("authorization", f"Bearer {jwt}"),))
350
-
351
-
352
- def update_order_list(
353
- order_list: list[list[Decimal]],
354
- price: Decimal,
355
- size: Decimal,
356
- ascending: bool,
357
- ) -> None:
358
- """
359
- Updates a sorted order list (either ascending for asks or descending for bids)
360
- using binary search to insert, update, or remove the given price level.
361
- """
362
- if ascending:
363
- idx = bisect.bisect_left(order_list, [price, Decimal(0)])
364
- else:
365
- lo, hi = 0, len(order_list)
366
- while lo < hi:
367
- mid = (lo + hi) // 2
368
- if order_list[mid][0] > price:
369
- lo = mid + 1
370
- else:
371
- hi = mid
372
- idx = lo
373
-
374
- if idx < len(order_list) and order_list[idx][0] == price:
375
- if size.is_zero():
376
- order_list.pop(idx)
377
- else:
378
- # Update the size.
379
- order_list[idx][1] = size
380
- else:
381
- if not size.is_zero():
382
- order_list.insert(idx, [price, size])
383
-
384
-
385
- def L2_update_from_diff(book: L2BookSnapshot, diff: L2BookDiff) -> None:
386
- """
387
- we use binary search because the L2 does not have many levels
388
- and is simpler to maintain in the context of the codegen
389
- """
390
-
391
- book.timestamp = diff.timestamp
392
- book.timestamp_ns = diff.timestamp_ns
393
- book.sequence_id = diff.sequence_id
394
- book.sequence_number = diff.sequence_number
395
-
396
- for price, size in diff.bids:
397
- update_order_list(book.bids, price, size, ascending=False)
398
-
399
- for price, size in diff.asks:
400
- update_order_list(book.asks, price, size, ascending=True)
401
-
402
-
403
- def update_struct(A: msgspec.Struct, B: msgspec.Struct) -> None:
404
- for field in B.__struct_fields__:
405
- setattr(A, field, getattr(B, field))