architect-py 3.2.2__py3-none-any.whl → 5.0.0b2__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 (205) hide show
  1. architect_py/__init__.py +15 -2
  2. architect_py/async_client.py +1060 -643
  3. architect_py/client.py +25 -26
  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 +143 -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 +2 -2
  52. architect_py/{grpc_client → grpc/models}/Cpty/CptyResponse.py +3 -3
  53. architect_py/{grpc_client → grpc/models}/Cpty/CptyStatus.py +1 -1
  54. architect_py/{grpc_client → grpc/models}/Cpty/CptyStatusRequest.py +2 -2
  55. architect_py/{grpc_client → grpc/models}/Cpty/CptysRequest.py +2 -2
  56. architect_py/{grpc_client → grpc/models}/Cpty/CptysResponse.py +1 -1
  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 +1 -1
  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 +1 -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 +690 -841
  138. architect_py/grpc/resolve_endpoint.py +70 -0
  139. architect_py/{grpc_client/grpc_server.py → grpc/server.py} +9 -6
  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 -87
  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.0b2.dist-info/METADATA +123 -0
  158. architect_py-5.0.0b2.dist-info/RECORD +214 -0
  159. {architect_py-3.2.2.dist-info → architect_py-5.0.0b2.dist-info}/WHEEL +2 -1
  160. architect_py-5.0.0b2.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 -53
  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/Health/__init__.py +0 -2
  184. architect_py/grpc_client/Marketdata/__init__.py +0 -2
  185. architect_py/grpc_client/Oms/Cancel.py +0 -42
  186. architect_py/grpc_client/Oms/__init__.py +0 -2
  187. architect_py/grpc_client/Orderflow/__init__.py +0 -2
  188. architect_py/grpc_client/Symbology/__init__.py +0 -2
  189. architect_py/grpc_client/__init__.py +0 -2
  190. architect_py/grpc_client/grpc_client.py +0 -413
  191. architect_py/scalars.py +0 -172
  192. architect_py/tests/test_accounts.py +0 -31
  193. architect_py/tests/test_client.py +0 -29
  194. architect_py/tests/test_grpc_client.py +0 -30
  195. architect_py/tests/test_order_sending.py +0 -65
  196. architect_py/tests/test_snapshots.py +0 -52
  197. architect_py/tests/test_subscriptions.py +0 -126
  198. architect_py-3.2.2.dist-info/METADATA +0 -191
  199. architect_py-3.2.2.dist-info/RECORD +0 -148
  200. /architect_py/{grpc_client → grpc/models}/Marketdata/ArrayOfL1BookSnapshot.py +0 -0
  201. /architect_py/{grpc_client → grpc/models}/Marketdata/L2BookUpdate.py +0 -0
  202. /architect_py/{grpc_client → grpc/models}/Marketdata/TickerUpdate.py +0 -0
  203. /architect_py/{grpc_client → grpc/models}/Orderflow/Dropcopy.py +0 -0
  204. /architect_py/{grpc_client → grpc/models}/Orderflow/Orderflow.py +0 -0
  205. {architect_py-3.2.2.dist-info → architect_py-5.0.0b2.dist-info/licenses}/LICENSE +0 -0
@@ -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,413 +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.Symbology.SymbolsRequest import (
53
- SymbolsRequest,
54
- )
55
- from architect_py.grpc_client.definitions import L2BookDiff
56
- from architect_py.scalars import TradableProduct
57
-
58
-
59
- """
60
- get_account_summaries_for_cpty
61
- """
62
-
63
-
64
- def enc_hook(obj: Any) -> Any:
65
- if isinstance(obj, TradableProduct):
66
- return str(obj)
67
-
68
-
69
- encoder = msgspec.json.Encoder(enc_hook=enc_hook)
70
- ResponseTypeGeneric = TypeVar("ResponseTypeGeneric", covariant=True)
71
-
72
-
73
- class RequestType(Protocol[ResponseTypeGeneric]):
74
- @staticmethod
75
- def get_unannotated_response_type() -> Type[ResponseTypeGeneric]: ...
76
-
77
- @staticmethod
78
- def get_response_type() -> Type[ResponseTypeGeneric]: ...
79
-
80
- @staticmethod
81
- def get_route() -> str: ...
82
-
83
- @staticmethod
84
- def get_rpc_method() -> Any: ...
85
-
86
-
87
- class GRPCClient:
88
- jwt: str
89
- jwt_expiration: datetime
90
-
91
- graphql_client: GraphQLClient
92
- l1_books: dict[TradableProduct, L1BookSnapshot]
93
- l2_books: dict[TradableProduct, L2BookSnapshot]
94
- channel: grpc.aio.Channel
95
- _decoders: dict[type | UnionType, msgspec.json.Decoder]
96
-
97
- def __init__(
98
- self,
99
- graphql_client: GraphQLClient,
100
- endpoint: str = "cme.marketdata.architect.co",
101
- ):
102
- """
103
- Please ensure to call the initialize method before using the gRPC client.
104
-
105
- grpc_client = GRPCClient(graphql_client, endpoint)
106
- await grpc_client.initialize()
107
-
108
-
109
- Brave users may create their own requests using the subscribe and request methods.
110
- The types are correct so if a typechecker such as PyLance is throwing errors,
111
- it's likely a bug in user code.
112
-
113
- async for snap in self.subscribe(
114
- RequestType.get_request_helper(), # add args/kwargs here
115
- ):
116
-
117
- snap = await self.request(
118
- RequestType.get_request_helper(), # add args/kwargs here
119
- )
120
- """
121
- self.graphql_client = graphql_client
122
-
123
- self.jwt_expiration = datetime(1995, 11, 10)
124
-
125
- self.l1_books: dict[TradableProduct, L1BookSnapshot] = {}
126
- self.l2_books: dict[TradableProduct, L2BookSnapshot] = {}
127
- self.endpoint = endpoint
128
-
129
- self._decoders: dict[type | UnionType, msgspec.json.Decoder] = {}
130
-
131
- async def initialize(self) -> Optional[str]:
132
- """
133
- Initialize the gRPC channel with the given endpoint.
134
- Must call this method before using the gRPC client.
135
- """
136
- # "binance-futures-usd-m.marketdata.architect.co",
137
- # "https://usdm.binance.marketdata.architect.co"
138
- # "bybit.marketdata.architect.co",
139
- # "binance.marketdata.architect.co",
140
- # "cme.marketdata.architect.co",
141
- self.channel = await self.get_grpc_channel(self.endpoint)
142
-
143
- async def change_channel(self, endpoint: str) -> None:
144
- self.channel = await self.get_grpc_channel(endpoint)
145
-
146
- async def get_grpc_channel(
147
- self,
148
- endpoint: str,
149
- ) -> grpc.aio.Channel:
150
- if "://" not in endpoint:
151
- endpoint = f"http://{endpoint}"
152
- url = urlparse(endpoint)
153
- if url.hostname is None:
154
- raise Exception(f"Invalid endpoint: {endpoint}")
155
-
156
- is_https = url.scheme == "https"
157
- srv_records: dns.resolver.Answer = await dns.asyncresolver.resolve(
158
- url.hostname, "SRV"
159
- )
160
- if len(srv_records) == 0:
161
- raise Exception(f"No SRV records found for {url.hostname}")
162
-
163
- record = cast(SRV, srv_records[0])
164
-
165
- connect_str = f"{record.target}:{record.port}"
166
- if is_https:
167
- credentials = grpc.ssl_channel_credentials()
168
- return grpc.aio.secure_channel(connect_str, credentials)
169
- else:
170
- return grpc.aio.insecure_channel(connect_str)
171
-
172
- async def refresh_grpc_credentials(self, force: bool = False) -> str:
173
- """
174
- Refresh the JWT for the gRPC channel if it's nearing expiration (within 1 minute).
175
- If force is True, refresh the JWT unconditionally.
176
- """
177
- if force or datetime.now() > self.jwt_expiration - timedelta(minutes=1):
178
- try:
179
- self.jwt = (await self.graphql_client.create_jwt()).create_jwt
180
- self.jwt_expiration = datetime.now() + timedelta(hours=23)
181
- except Exception as e:
182
- logger.error("Failed to refresh gRPC credentials: %s", e)
183
- return self.jwt
184
-
185
- def get_decoder(
186
- self,
187
- response_type: type[ResponseTypeGeneric] | UnionType,
188
- ) -> msgspec.json.Decoder:
189
- try:
190
- return self._decoders[response_type]
191
- except KeyError:
192
- # we use a try / except because we sacrifice first time query
193
- # to optimize for repeated lookups
194
- decoder = msgspec.json.Decoder(type=response_type)
195
- self._decoders[response_type] = decoder
196
- return decoder
197
-
198
- async def symbols(self) -> list[str]:
199
- request = SymbolsRequest()
200
- response = await self.request(request)
201
- return response.symbols
202
-
203
- async def request_l1_book_snapshot(self, symbol: TradableProduct) -> L1BookSnapshot:
204
- request = L1BookSnapshotRequest(symbol=symbol)
205
- return await self.request(request)
206
-
207
- async def request_l2_book_snapshot(
208
- self, venue: Optional[str], symbol: TradableProduct
209
- ) -> L2BookSnapshot:
210
- request = L2BookSnapshotRequest(venue=venue, symbol=symbol)
211
- return await self.request(request)
212
-
213
- def initialize_l1_books(
214
- self, symbols: list[TradableProduct]
215
- ) -> list[L1BookSnapshot]:
216
- if symbols is not None:
217
- if len(self.l1_books) + len(symbols) > 100:
218
- raise ValueError(
219
- "Not suggestible to watch more than 100 L1 symbols at once, as it may cause performance issues."
220
- )
221
- symbols = [symbol for symbol in symbols if symbol not in self.l1_books]
222
-
223
- for symbol in symbols:
224
- self.l1_books[symbol] = L1BookSnapshot(symbol, 0, 0)
225
- else:
226
- raise ValueError("symbols must be a list of TradableProduct")
227
- # could technically be None, but we don't want to allow that
228
- # as users should be explicit about what they want to watch
229
-
230
- return [self.l1_books[symbol] for symbol in symbols]
231
-
232
- async def watch_l1_books(self, symbols: list[TradableProduct]) -> None:
233
- symbols_cast = cast(list[str], symbols)
234
-
235
- request = SubscribeL1BookSnapshotsRequest(symbols=symbols_cast)
236
-
237
- async for snap in self.subscribe(request):
238
- book = self.l1_books[cast(TradableProduct, snap.symbol)]
239
- update_struct(book, snap)
240
-
241
- def initialize_l2_book(
242
- self, symbol: TradableProduct, venue: Optional[str]
243
- ) -> L2BookSnapshot:
244
- if symbol not in self.l2_books:
245
- if len(self.l2_books) > 20:
246
- raise ValueError(
247
- "Not suggestible to watch more than 20 L2 symbols at once, as it may cause performance issues."
248
- )
249
- self.l2_books[symbol] = L2BookSnapshot([], [], 0, 0, 0, 0)
250
- return self.l2_books[symbol]
251
-
252
- async def subscribe_l1_books_stream(
253
- self, symbols: list[str]
254
- ) -> AsyncIterator[L1BookSnapshot]:
255
- request = SubscribeL1BookSnapshotsRequest(symbols=symbols)
256
- return self.subscribe(
257
- request,
258
- )
259
-
260
- async def subscribe_l2_books_stream(
261
- self, symbol: TradableProduct, venue: Optional[str]
262
- ) -> AsyncIterator[L2BookUpdate]:
263
- decoder: msgspec.json.Decoder[L2BookUpdate] = self.get_decoder(
264
- SubscribeL2BookUpdatesRequest.get_unannotated_response_type()
265
- )
266
- stub = self.channel.unary_stream(
267
- SubscribeL2BookUpdatesRequest.get_route(),
268
- request_serializer=encoder.encode,
269
- response_deserializer=decoder.decode,
270
- )
271
- req = SubscribeL2BookUpdatesRequest(symbol=symbol, venue=venue)
272
- jwt = await self.refresh_grpc_credentials()
273
- call = stub(req, metadata=(("authorization", f"Bearer {jwt}"),))
274
- async for update in call:
275
- yield update
276
-
277
- async def watch_l2_book(
278
- self, symbol: TradableProduct, venue: Optional[str]
279
- ) -> None:
280
- async for up in self.subscribe_l2_books_stream(symbol, venue):
281
- if isinstance(up, L2BookDiff): # elif up.t = "d": # diff
282
- if symbol not in self.l2_books:
283
- raise ValueError(
284
- f"received update before snapshot for L2 book {symbol}"
285
- )
286
- book = self.l2_books[symbol]
287
- if (
288
- up.sequence_id != book.sequence_id
289
- or up.sequence_number != book.sequence_number + 1
290
- ):
291
- raise ValueError(
292
- f"received update out of order for L2 book {symbol}"
293
- )
294
- L2_update_from_diff(book, up)
295
- elif isinstance(up, L2BookSnapshot): # if up.t = "s":
296
- book = self.l2_books[symbol]
297
- update_struct(book, up)
298
-
299
- async def subscribe_orderflow_stream(
300
- self, request_iterator: AsyncIterator[OrderflowRequest]
301
- ) -> AsyncIterator[Orderflow]:
302
- """
303
- subscribe_orderflow_stream is a duplex_stream meaning that it is a stream that can be read from and written to.
304
- This is a stream that will be used to send orders to the Architect and receive order updates from the Architect.
305
- """
306
- decoder = self.get_decoder(OrderflowRequestUnannotatedResponseType)
307
- stub = self.channel.stream_stream(
308
- OrderflowRequest_route,
309
- request_serializer=encoder.encode,
310
- response_deserializer=decoder.decode,
311
- )
312
- jwt = await self.refresh_grpc_credentials()
313
- call = stub(request_iterator, metadata=(("authorization", f"Bearer {jwt}"),))
314
- async for update in call:
315
- yield update
316
-
317
- async def subscribe(
318
- self,
319
- request: RequestType[ResponseTypeGeneric],
320
- ) -> AsyncIterator[ResponseTypeGeneric]:
321
- """
322
- Generic function for subscribing to a stream of updates from the gRPC server.
323
-
324
- request_type and ResponseTypeGeneric *cannot* be union types
325
- """
326
- decoder: msgspec.json.Decoder[ResponseTypeGeneric] = self.get_decoder(
327
- request.get_unannotated_response_type()
328
- )
329
- stub = self.channel.unary_stream(
330
- request.get_route(),
331
- request_serializer=encoder.encode,
332
- response_deserializer=decoder.decode,
333
- )
334
- jwt = await self.refresh_grpc_credentials()
335
- call = stub(request, metadata=(("authorization", f"Bearer {jwt}"),))
336
- async for update in call:
337
- yield update
338
-
339
- async def request(
340
- self,
341
- request: RequestType[ResponseTypeGeneric],
342
- ) -> ResponseTypeGeneric:
343
- """
344
- Generic function for making a unary request to the gRPC server.
345
-
346
- request_type and ResponseTypeGeneric *cannot* be union types
347
- """
348
- decoder: msgspec.json.Decoder[ResponseTypeGeneric] = self.get_decoder(
349
- request.get_unannotated_response_type()
350
- )
351
- stub = self.channel.unary_unary(
352
- request.get_route(),
353
- request_serializer=encoder.encode,
354
- response_deserializer=decoder.decode,
355
- )
356
- jwt = await self.refresh_grpc_credentials()
357
- return await stub(request, metadata=(("authorization", f"Bearer {jwt}"),))
358
-
359
-
360
- def update_order_list(
361
- order_list: list[list[Decimal]],
362
- price: Decimal,
363
- size: Decimal,
364
- ascending: bool,
365
- ) -> None:
366
- """
367
- Updates a sorted order list (either ascending for asks or descending for bids)
368
- using binary search to insert, update, or remove the given price level.
369
- """
370
- if ascending:
371
- idx = bisect.bisect_left(order_list, [price, Decimal(0)])
372
- else:
373
- lo, hi = 0, len(order_list)
374
- while lo < hi:
375
- mid = (lo + hi) // 2
376
- if order_list[mid][0] > price:
377
- lo = mid + 1
378
- else:
379
- hi = mid
380
- idx = lo
381
-
382
- if idx < len(order_list) and order_list[idx][0] == price:
383
- if size.is_zero():
384
- order_list.pop(idx)
385
- else:
386
- # Update the size.
387
- order_list[idx][1] = size
388
- else:
389
- if not size.is_zero():
390
- order_list.insert(idx, [price, size])
391
-
392
-
393
- def L2_update_from_diff(book: L2BookSnapshot, diff: L2BookDiff) -> None:
394
- """
395
- we use binary search because the L2 does not have many levels
396
- and is simpler to maintain in the context of the codegen
397
- """
398
-
399
- book.timestamp = diff.timestamp
400
- book.timestamp_ns = diff.timestamp_ns
401
- book.sequence_id = diff.sequence_id
402
- book.sequence_number = diff.sequence_number
403
-
404
- for price, size in diff.bids:
405
- update_order_list(book.bids, price, size, ascending=False)
406
-
407
- for price, size in diff.asks:
408
- update_order_list(book.asks, price, size, ascending=True)
409
-
410
-
411
- def update_struct(A: msgspec.Struct, B: msgspec.Struct) -> None:
412
- for field in B.__struct_fields__:
413
- setattr(A, field, getattr(B, field))
architect_py/scalars.py DELETED
@@ -1,172 +0,0 @@
1
- from enum import Enum
2
- from typing import Literal, Optional
3
-
4
- from datetime import datetime, timezone
5
-
6
-
7
- from typing import TYPE_CHECKING
8
-
9
- if TYPE_CHECKING:
10
- from architect_py.graphql_client.base_model import UnsetType
11
-
12
-
13
- """
14
- Custom Serialize / Deserializing functions for the scalars
15
- """
16
-
17
-
18
- def serialize(value) -> str:
19
- return value.serialize()
20
-
21
-
22
- class TradableProduct(str):
23
- """
24
- Example instantiations:
25
- TradableProduct("ES 20250321 CME Future", "USD")
26
- TradableProduct("ES 20250321 CME Future/USD")
27
- (these are equivalent)
28
-
29
- This type exists to enforce the
30
- {base}/{quote} format for strings
31
-
32
- A base is the product that is being priced in terms of the quote.
33
- For example,
34
- "ES 20250321 CME Future/USD" means that the ES 20250321 CME Future is priced in USD.
35
- "ES 20250321 CME Future/EUR" means that the ES 20250321 CME Future is priced in EUR.
36
- "ES 20250321 CME Future/BTC" means that the ES 20250321 CME Future is priced in BTC
37
- (such a product does not exist on any exchange though).
38
-
39
- For example in a currency pair, the base is the first currency and the quote is the second currency.
40
- In the currency pair USD/EUR, USD is the base and EUR is the quote.
41
- USD/EUR = 1.1234 means that 1 USD = 1.1234 EUR
42
- EUR/USD = 0.8901 means that 1 EUR = 0.8901 USD
43
- """
44
-
45
- def __new__(
46
- cls, base_or_value: str, quote: Optional[str] = None
47
- ) -> "TradableProduct":
48
- """
49
- These are equivalent:
50
- TradableProduct("ES 20250321 CME Future", "USD")
51
- TradableProduct("ES 20250321 CME Future/USD")
52
- """
53
- if quote is None:
54
- value = base_or_value
55
- else:
56
- value = f"{base_or_value}/{quote}"
57
-
58
- assert (
59
- "/" in value
60
- ), f"TradableProduct must be in the form of 'base/quote'. Got: {base_or_value}"
61
- return super().__new__(cls, value)
62
-
63
- def base_quote(self) -> list[str]:
64
- return self.split("/")
65
-
66
- def base(self) -> str:
67
- return self.split("/", 1)[0]
68
-
69
- def quote(self) -> str:
70
- return self.split("/", 1)[1]
71
-
72
-
73
- def parse_tradable_product(value: str) -> TradableProduct:
74
- # for ariadne
75
- return TradableProduct(value)
76
-
77
-
78
- class OrderDir(str, Enum):
79
- BUY = "BUY"
80
- SELL = "SELL"
81
-
82
- def __int__(self):
83
- if self == OrderDir.BUY:
84
- return 1
85
- elif self == OrderDir.SELL:
86
- return -1
87
- else:
88
- raise ValueError(f"Unknown Dir: {self}")
89
-
90
- def get_opposite(self) -> "OrderDir":
91
- """
92
- NOTE: ENUMS ARE IMMUTABLE SO THIS DOES NOT MUTATE THE STATE OF THE ENUM
93
- """
94
- if self == OrderDir.BUY:
95
- return OrderDir.SELL
96
- elif self == OrderDir.SELL:
97
- return OrderDir.BUY
98
- else:
99
- raise ValueError(f"Unknown Dir: {self}")
100
-
101
- def __str__(self) -> str:
102
- return self.value
103
-
104
- def lower(self) -> str:
105
- return self.value.lower()
106
-
107
- @classmethod
108
- def from_string(cls, value: str) -> "OrderDir":
109
- lower = value.lower()
110
- if lower == "buy":
111
- return cls.BUY
112
- elif lower == "sell":
113
- return cls.SELL
114
- elif lower == "bid":
115
- return cls.BUY
116
- elif lower == "ask":
117
- return cls.SELL
118
- elif lower == "b":
119
- return cls.BUY
120
- elif lower == "a" or lower == "s":
121
- return cls.SELL
122
- else:
123
- raise ValueError(f"Unknown Dir: {value}")
124
-
125
- @classmethod
126
- def from_unit(cls, value: Literal[1, -1]) -> "OrderDir":
127
- if value == 1:
128
- return cls.BUY
129
- elif value == -1:
130
- return cls.SELL
131
- else:
132
- raise ValueError(f"Unknown Dir: {value}")
133
-
134
- @classmethod
135
- def from_sign(cls, value: int) -> "OrderDir":
136
- if value > 0:
137
- return cls.BUY
138
- elif value < 0:
139
- return cls.SELL
140
- else:
141
- raise ValueError(f"Unknown Dir: {value}")
142
-
143
-
144
- def graphql_serialize_order_dir(value: OrderDir) -> str:
145
- return value.lower()
146
-
147
-
148
- def graphql_parse_order_dir(value: str) -> OrderDir:
149
- if value == "buy":
150
- return OrderDir.BUY
151
- else:
152
- return OrderDir.SELL
153
-
154
-
155
- def convert_datetime_to_utc_str(dt: "Optional[datetime] | UnsetType") -> Optional[str]:
156
- if not isinstance(dt, datetime):
157
- return None
158
-
159
- if dt.tzinfo is None:
160
- raise ValueError(
161
- "in a datetime sent to the backend, the good_til_date must be timezone-aware. Try \n"
162
- "from zoneinfo import ZoneInfo\n"
163
- "datetime(..., tzinfo={your_local_timezone}) or "
164
- "datetime.now(tz=ZoneInfo('UTC'))\n"
165
- "# examples of local timezones:\n"
166
- "ZoneInfo('America/New_York'), "
167
- "ZoneInfo('America/Los_Angeles'), ZoneInfo('America/Chicago')"
168
- )
169
- utc_str = dt.astimezone(timezone.utc).isoformat()[:-6]
170
- # [:-6] removes the utc offset
171
-
172
- return f"{utc_str}Z"
@@ -1,31 +0,0 @@
1
- from decimal import Decimal
2
- import pytest
3
-
4
- from architect_py.async_client import AsyncClient
5
-
6
- from collections import defaultdict
7
-
8
-
9
- @pytest.mark.asyncio
10
- async def test_account(async_client: AsyncClient):
11
- return
12
-
13
- accounts = await async_client.list_accounts()
14
- assert accounts is not None
15
- assert len(accounts) > 0
16
-
17
- unaggregated_summary = await async_client.get_account_summary(
18
- account=accounts[0].account.id
19
- )
20
-
21
- symbol_to_position = defaultdict(Decimal)
22
- for symbol, positions in unaggregated_summary.positions.items():
23
- for position in positions:
24
- symbol_to_position[symbol] += position.quantity
25
-
26
- aggregated_summary = await async_client.get_account_summary(
27
- account=accounts[0].account.id
28
- )
29
-
30
- assert unaggregated_summary is not None
31
- assert aggregated_summary is not None
@@ -1,29 +0,0 @@
1
- import pytest
2
- from architect_py.async_client import AsyncClient
3
- from architect_py.client import Client
4
-
5
-
6
- @pytest.mark.asyncio
7
- async def test_client_init():
8
- with pytest.raises(ValueError):
9
- client = AsyncClient(host="localhost", port=4567, api_key=" ", api_secret=" ")
10
- with pytest.raises(ValueError):
11
- client = AsyncClient(
12
- host="localhost", port=4567, api_key="something", api_secret='"alskjdf"'
13
- )
14
-
15
-
16
- @pytest.mark.asyncio
17
- async def test_client_jwt(async_client: AsyncClient):
18
- jwt = await async_client.graphql_client.create_jwt()
19
- assert jwt is not None, "jwt should not be None"
20
-
21
-
22
- def test_sync_client(sync_client: Client):
23
- sync_result = sync_client.search_symbols(execution_venue="CME", search_string="ES")
24
- assert len(sync_result) > 5
25
-
26
- ES_future = sync_result[0]
27
-
28
- sync_result = sync_client.search_symbols(ES_future)
29
- assert sync_result is not None