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
@@ -0,0 +1,70 @@
1
+ import ipaddress
2
+ import logging
3
+ from typing import Tuple, cast
4
+ from urllib.parse import urlparse
5
+
6
+ import dns.asyncresolver
7
+ import dns.resolver
8
+ from dns.rdtypes.IN.SRV import SRV
9
+
10
+
11
+ async def resolve_endpoint(endpoint: str) -> Tuple[str, int, bool]:
12
+ """
13
+ From a gRPC endpoint, resolve the host, port and whether or not the endpoint
14
+ should use SSL. If the port is specified explicitly, it will be used. Otherwise,
15
+ try to look up the port using the host's DNS SRV record.
16
+
17
+ If no SRV DNS record exists, or the hostname is an IP address, port will be None.
18
+
19
+ If the endpoint scheme is https, or the hostname matches *.architect.co,
20
+ return True for use_ssl. Otherwise, return False.
21
+
22
+ Example outputs:
23
+
24
+ Assuming a SRV DNS record exists for app.architect.co pointing to port 8081.
25
+
26
+ | Endpoint | Host | Port | Use SSL |
27
+ |----------|------|------|---------|
28
+ | https://app.architect.co:8081 | app.architect.co | 8081 | True |
29
+ | http://app.architect.co:8081 | app.architect.co | 8081 | False |
30
+ | app.architect.co | app.architect.co | 8081 | True |
31
+ | localhost:9000 | localhost | 9000 | False |
32
+ """
33
+ if "://" not in endpoint:
34
+ endpoint = f"unknown://{endpoint}"
35
+ url = urlparse(endpoint)
36
+
37
+ if url.hostname is None:
38
+ raise ValueError(f"Invalid endpoint, missing hostname: {endpoint}")
39
+
40
+ use_ssl = url.scheme == "https" or (
41
+ url.scheme != "http" and url.hostname.endswith(".architect.co")
42
+ )
43
+
44
+ try:
45
+ _ = ipaddress.ip_address(url.hostname)
46
+ if url.port is None:
47
+ raise ValueError(
48
+ f"Invalid endpoint, target is an IP address but missing port: {endpoint}"
49
+ )
50
+ return url.hostname, url.port, url.scheme == "https"
51
+ except ValueError:
52
+ # not an IP address
53
+ pass
54
+
55
+ if url.port is not None:
56
+ return url.hostname, url.port, use_ssl
57
+
58
+ logging.info(f"No port specified for {endpoint}, looking up DNS SRV records...")
59
+ srv_records: dns.resolver.Answer = await dns.asyncresolver.resolve(
60
+ url.hostname, "SRV"
61
+ )
62
+ if len(srv_records) == 0:
63
+ raise Exception(f"No SRV records found for {url.hostname}")
64
+
65
+ record = cast(SRV, srv_records[0])
66
+ logging.info(f"Found {endpoint}: {record.target}:{record.port}")
67
+
68
+ host = str(record.target).rstrip(".") # strips the period off of FQDNs
69
+
70
+ return host, record.port, use_ssl
@@ -1,9 +1,14 @@
1
+ """
2
+ Utility functions for hosting an Architect gRPC server;
3
+ external cptys, python-based marketdata feeds, etc.
4
+ """
5
+
1
6
  import grpc
2
7
  import msgspec
3
- from typing import Union
4
- from .grpc_client import encoder
5
- from .Cpty.CptyRequest import Login, Logout, PlaceOrder, CancelOrder
6
- from .Orderflow.SubscribeOrderflowRequest import SubscribeOrderflowRequest
8
+
9
+ from .models.Cpty.CptyRequest import UnannotatedCptyRequest
10
+ from .models.Orderflow.SubscribeOrderflowRequest import SubscribeOrderflowRequest
11
+ from .utils import encoder
7
12
 
8
13
 
9
14
  class CptyServicer(object):
@@ -14,8 +19,7 @@ class CptyServicer(object):
14
19
 
15
20
 
16
21
  def add_CptyServicer_to_server(servicer, server):
17
- # CR alee: there must be a better way to get the unannotated request type
18
- decoder = msgspec.json.Decoder(type=Union[Login, Logout, PlaceOrder, CancelOrder])
22
+ decoder = msgspec.json.Decoder(type=UnannotatedCptyRequest)
19
23
  rpc_method_handlers = {
20
24
  "Cpty": grpc.stream_stream_rpc_method_handler(
21
25
  servicer.Cpty,
@@ -37,9 +41,9 @@ class OrderflowServicer(object):
37
41
 
38
42
 
39
43
  def add_OrderflowServicer_to_server(servicer, server):
40
- # CR alee: this doesn't work
41
- # decoder = msgspec.json.Decoder(type=SubscribeOrderflowRequest.get_unannotated_response_type())
42
- decoder = msgspec.json.Decoder(type=dict)
44
+ decoder = msgspec.json.Decoder(
45
+ type=SubscribeOrderflowRequest.get_unannotated_response_type()
46
+ )
43
47
  rpc_method_handlers = {
44
48
  "SubscribeOrderflow": grpc.unary_stream_rpc_method_handler(
45
49
  servicer.SubscribeOrderflow,
@@ -0,0 +1,32 @@
1
+ from types import UnionType
2
+ from typing import Any, Protocol, Type, TypeVar
3
+
4
+ import msgspec
5
+
6
+ from architect_py.common_types import TradableProduct
7
+
8
+
9
+ def enc_hook(obj: Any) -> Any:
10
+ if isinstance(obj, TradableProduct):
11
+ return str(obj)
12
+
13
+
14
+ encoder = msgspec.json.Encoder(enc_hook=enc_hook)
15
+ decoders: dict[type | UnionType, msgspec.json.Decoder] = {}
16
+
17
+
18
+ ResponseTypeGeneric = TypeVar("ResponseTypeGeneric", covariant=True)
19
+
20
+
21
+ class RequestType(Protocol[ResponseTypeGeneric]):
22
+ @staticmethod
23
+ def get_unannotated_response_type() -> Type[ResponseTypeGeneric]: ...
24
+
25
+ @staticmethod
26
+ def get_response_type() -> Type[ResponseTypeGeneric]: ...
27
+
28
+ @staticmethod
29
+ def get_route() -> str: ...
30
+
31
+ @staticmethod
32
+ def get_rpc_method() -> Any: ...
File without changes
@@ -0,0 +1,3 @@
1
+ from typing import Never
2
+
3
+ DataFrame = Never
@@ -1,118 +1,124 @@
1
- from datetime import datetime
2
- import os
1
+ """
2
+ Tests for architect-py can be run against any particular environment.
3
3
 
4
- import pytest
5
- import pytest_asyncio
6
- from architect_py.client import Client
7
- from architect_py.async_client import AsyncClient
8
- from dotenv import load_dotenv
4
+ Set the following environments variables before running pytest:
9
5
 
10
- from architect_py.async_client import AsyncClient
6
+ - ARCHITECT_ENDPOINT
7
+ - ARCHITECT_API_KEY
8
+ - ARCHITECT_API_SECRET
9
+ - ARCHITECT_PAPER_TRADING
11
10
 
11
+ Optional environment varaibles:
12
12
 
13
- """
14
- if you have a file named ".env" in your working directory with:
13
+ - ARCHITECT_GRAPHQL_PORT (default=4567)
14
+ - DANGEROUS_ALLOW_LIVE_TRADING (default=False)
15
15
 
16
- ARCHITECT_API_KEY=your_key
17
- ARCHITECT_API_SECRET=your_secret
18
- PAPER_TRADING=False
16
+ Environment variables may be set in a `.env` file.
19
17
  """
20
18
 
19
+ import os
20
+ from datetime import datetime, timedelta
21
21
 
22
- def is_truthy(value: str | None) -> bool:
23
- return value is not None and value.lower() in ("1", "true", "yes")
22
+ import pytest_asyncio
23
+ from dotenv import load_dotenv
24
24
 
25
+ from architect_py import AsyncClient
25
26
 
26
- @pytest_asyncio.fixture
27
- async def async_client() -> AsyncClient:
28
- load_dotenv()
29
27
 
30
- host = os.getenv("ARCHITECT_HOST") or "localhost"
31
- port = os.getenv(key="ARCHITECT_PORT")
32
- if port is not None:
33
- port = int(port)
28
+ def is_truthy(value: str | None) -> bool:
29
+ return value is not None and value.lower() in ("1", "true", "yes")
30
+
34
31
 
35
- api_key = os.getenv("ARCHITECT_API_KEY")
36
- api_secret = os.getenv("ARCHITECT_API_SECRET")
37
- paper_trading = os.getenv("ARCHITECT_PAPER_TRADING")
38
- if paper_trading is None:
39
- paper_trading = True
40
- else:
41
- paper_trading = is_truthy(paper_trading)
32
+ class TestEnvironment:
33
+ @classmethod
34
+ def from_env(cls):
35
+ endpoint = os.getenv("ARCHITECT_ENDPOINT")
36
+ api_key = os.getenv("ARCHITECT_API_KEY")
37
+ api_secret = os.getenv("ARCHITECT_API_SECRET")
42
38
 
43
- dangerous_allow_app_architect_co = os.getenv("DANGEROUS_ALLOW_APP_ARCHITECT_CO")
39
+ assert endpoint is not None
40
+ assert api_key is not None
41
+ assert api_secret is not None
44
42
 
45
- if host == "app.architect.co" and not is_truthy(dangerous_allow_app_architect_co):
46
- raise ValueError(
47
- "You have set the HOST to the production server. Please change it to the sandbox server."
43
+ paper_trading = is_truthy(os.getenv("ARCHITECT_PAPER_TRADING"))
44
+ dangerous_allow_live_trading = is_truthy(
45
+ os.getenv("DANGEROUS_ALLOW_LIVE_TRADING")
48
46
  )
49
- if api_key is None or api_secret is None:
50
- raise ValueError(
51
- "You must set ARCHITECT_API_KEY and ARCHITECT_API_SECRET to run tests"
47
+ graphql_port = os.getenv("ARCHITECT_GRAPHQL_PORT")
48
+ if graphql_port is not None:
49
+ graphql_port = int(graphql_port)
50
+
51
+ if not paper_trading and not dangerous_allow_live_trading:
52
+ raise ValueError(
53
+ "You must set ARCHITECT_PAPER_TRADING=True or DANGEROUS_ALLOW_LIVE_TRADING=True to run live tests"
54
+ )
55
+
56
+ return cls(
57
+ endpoint=endpoint,
58
+ api_key=api_key,
59
+ api_secret=api_secret,
60
+ paper_trading=paper_trading,
61
+ graphql_port=graphql_port,
52
62
  )
53
63
 
54
- return await AsyncClient.connect(
55
- host=host,
56
- _port=port,
57
- api_key=api_key,
58
- api_secret=api_secret,
59
- paper_trading=paper_trading,
60
- )
64
+ def __init__(
65
+ self,
66
+ *,
67
+ endpoint: str,
68
+ api_key: str,
69
+ api_secret: str,
70
+ paper_trading: bool,
71
+ graphql_port: int | None,
72
+ ):
73
+ self.endpoint = endpoint
74
+ self.api_key = api_key
75
+ self.api_secret = api_secret
76
+ self.paper_trading = paper_trading
77
+ self.graphql_port = graphql_port
61
78
 
62
79
 
63
- @pytest.fixture
64
- def sync_client():
80
+ @pytest_asyncio.fixture
81
+ async def async_client() -> AsyncClient:
65
82
  load_dotenv()
66
- host = os.getenv("ARCHITECT_HOST") or "localhost"
67
- port = os.getenv(key="ARCHITECT_PORT")
68
- if port is not None:
69
- port = int(port)
83
+ test_env = TestEnvironment.from_env()
84
+ async_client = await AsyncClient.connect(
85
+ api_key=test_env.api_key,
86
+ api_secret=test_env.api_secret,
87
+ paper_trading=test_env.paper_trading,
88
+ endpoint=test_env.endpoint,
89
+ graphql_port=test_env.graphql_port,
90
+ )
70
91
 
71
- api_key = os.getenv("ARCHITECT_API_KEY")
72
- api_secret = os.getenv("ARCHITECT_API_SECRET")
92
+ marketdata_endpoints = (("BINANCE/USDM", "usdm.binance.marketdata.architect.co"),)
93
+ for venue, endpoint in marketdata_endpoints:
94
+ await async_client.set_marketdata(venue, endpoint)
73
95
 
74
- paper_trading = os.getenv("ARCHITECT_PAPER_TRADING")
75
- if paper_trading is None:
76
- paper_trading = True
77
- else:
78
- paper_trading = is_truthy(paper_trading)
96
+ return async_client
79
97
 
80
- dangerous_allow_app_architect_co = os.getenv("DANGEROUS_ALLOW_APP_ARCHITECT_CO")
81
98
 
82
- if host == "app.architect.co" and not is_truthy(dangerous_allow_app_architect_co):
83
- raise ValueError(
84
- "You have set the HOST to the production server. Please change it to the sandbox server."
85
- )
86
- if api_key is None or api_secret is None:
87
- raise ValueError(
88
- "You must set ARCHITECT_API_KEY and ARCHITECT_API_SECRET to run tests"
89
- )
90
- return Client(
91
- host=host,
92
- api_key=api_key,
93
- api_secret=api_secret,
94
- _port=port,
95
- paper_trading=paper_trading,
96
- )
99
+ async def get_front_ES_future(async_client: AsyncClient) -> str:
100
+ series = await async_client.get_cme_futures_series("ES CME Futures")
101
+ series.sort()
102
+ today = datetime.now() + timedelta(days=30)
103
+ unexpired = list(filter(lambda item: item[0] > today.date(), series))
104
+ return unexpired[0][1]
97
105
 
98
106
 
99
107
  @pytest_asyncio.fixture
100
108
  async def front_ES_future(async_client: AsyncClient) -> str:
101
- series = await async_client.get_cme_futures_series("ES CME Futures")
102
-
103
- inc = 0
104
- if datetime.now().date() >= async_client.get_expiration_from_CME_name(series[0][1]):
105
- inc += 1
106
-
107
- return series[inc][1]
109
+ """
110
+ Fixture for getting the name of the front month ES CME future.
111
+ """
112
+ return await get_front_ES_future(async_client)
108
113
 
109
114
 
110
115
  @pytest_asyncio.fixture
111
- async def front_ES_future_tp(async_client: AsyncClient) -> str:
112
- series = await async_client.get_cme_futures_series("ES CME Futures")
116
+ async def front_ES_future_usd(async_client: AsyncClient) -> str:
117
+ """
118
+ Fixture for getting the name of the front month ES CME future/USD pair.
119
+ """
120
+ future = await get_front_ES_future(async_client)
121
+ return f"{future}/USD"
113
122
 
114
- inc = 0
115
- if datetime.now().date() >= async_client.get_expiration_from_CME_name(series[0][1]):
116
- inc += 1
117
123
 
118
- return f"{series[inc][1]}/USD"
124
+ # CR alee: add sync Client tests
@@ -1,56 +1,55 @@
1
- from decimal import Decimal
2
- import pytest
1
+ # import pytest
2
+ # from decimal import Decimal
3
+ # from architect_py.grpc_client.Marketdata.L2BookUpdate import Diff, Snapshot
4
+ # from architect_py.grpc_client.grpc_client import L2_update_from_diff
3
5
 
4
- from architect_py.grpc_client.Marketdata.L2BookUpdate import Diff, Snapshot
5
- from architect_py.grpc_client.grpc_client import L2_update_from_diff
6
6
 
7
+ # @pytest.mark.asyncio
8
+ # async def test_book_build():
9
+ # snapshot = Snapshot(
10
+ # a=[],
11
+ # b=[],
12
+ # sid=0,
13
+ # sn=0,
14
+ # ts=0,
15
+ # tn=0,
16
+ # )
7
17
 
8
- @pytest.mark.asyncio
9
- async def test_book_build():
10
- snapshot = Snapshot(
11
- a=[],
12
- b=[],
13
- sid=0,
14
- sn=0,
15
- ts=0,
16
- tn=0,
17
- )
18
+ # L2_update_from_diff(
19
+ # snapshot,
20
+ # Diff(
21
+ # a=[[Decimal(1), Decimal(1)]],
22
+ # b=[],
23
+ # sid=0,
24
+ # sn=0,
25
+ # ts=0,
26
+ # tn=0,
27
+ # ),
28
+ # )
18
29
 
19
- L2_update_from_diff(
20
- snapshot,
21
- Diff(
22
- a=[[Decimal(1), Decimal(1)]],
23
- b=[],
24
- sid=0,
25
- sn=0,
26
- ts=0,
27
- tn=0,
28
- ),
29
- )
30
+ # assert snapshot == Snapshot(
31
+ # a=[[Decimal(1), Decimal(1)]],
32
+ # b=[],
33
+ # sid=0,
34
+ # sn=0,
35
+ # ts=0,
36
+ # tn=0,
37
+ # )
30
38
 
31
- assert snapshot == Snapshot(
32
- a=[[Decimal(1), Decimal(1)]],
33
- b=[],
34
- sid=0,
35
- sn=0,
36
- ts=0,
37
- tn=0,
38
- )
39
+ # snapshot = Snapshot(
40
+ # a=[[Decimal(3), Decimal(3)], [Decimal(4), Decimal(4)]],
41
+ # b=[[Decimal(1), Decimal(1)], [Decimal(2), Decimal(2)]],
42
+ # sid=0,
43
+ # sn=0,
44
+ # ts=0,
45
+ # tn=0,
46
+ # )
39
47
 
40
- snapshot = Snapshot(
41
- a=[[Decimal(3), Decimal(3)], [Decimal(4), Decimal(4)]],
42
- b=[[Decimal(1), Decimal(1)], [Decimal(2), Decimal(2)]],
43
- sid=0,
44
- sn=0,
45
- ts=0,
46
- tn=0,
47
- )
48
-
49
- diff = Diff(
50
- a=[[Decimal(3), Decimal(3)], [Decimal(4), Decimal(4)]],
51
- b=[[Decimal(1), Decimal(1)], [Decimal(2), Decimal(2)]],
52
- sid=0,
53
- sn=0,
54
- ts=0,
55
- tn=0,
56
- )
48
+ # diff = Diff(
49
+ # a=[[Decimal(3), Decimal(3)], [Decimal(4), Decimal(4)]],
50
+ # b=[[Decimal(1), Decimal(1)], [Decimal(2), Decimal(2)]],
51
+ # sid=0,
52
+ # sn=0,
53
+ # ts=0,
54
+ # tn=0,
55
+ # )
@@ -0,0 +1,168 @@
1
+ from datetime import datetime, timedelta, timezone
2
+
3
+ import pytest
4
+ from pytest_lazy_fixtures import lf
5
+
6
+ from architect_py import AsyncClient, CandleWidth
7
+
8
+
9
+ @pytest.mark.asyncio
10
+ @pytest.mark.parametrize(
11
+ "venue,symbol",
12
+ [
13
+ ("BINANCE/USDM", "BTC-USDT BINANCE Perpetual/USDT Crypto"),
14
+ ],
15
+ )
16
+ async def test_get_market_status(async_client: AsyncClient, venue: str, symbol: str):
17
+ market_status = await async_client.get_market_status(symbol, venue)
18
+ assert market_status is not None
19
+ # CR alee: this is broken upstream
20
+ # assert market_status.is_trading
21
+
22
+
23
+ @pytest.mark.asyncio
24
+ @pytest.mark.parametrize(
25
+ "venue,symbol",
26
+ [
27
+ ("CME", lf("front_ES_future_usd")),
28
+ ],
29
+ )
30
+ async def test_get_historical_candles(
31
+ async_client: AsyncClient, venue: str, symbol: str
32
+ ):
33
+ start = datetime.now(timezone.utc)
34
+ candles = await async_client.get_historical_candles(
35
+ symbol, venue, CandleWidth.OneHour, start - timedelta(hours=24), start
36
+ )
37
+ assert len(candles) > 0
38
+
39
+
40
+ @pytest.mark.asyncio
41
+ @pytest.mark.parametrize(
42
+ "venue,symbol",
43
+ [
44
+ ("BINANCE/USDM", "BTC-USDT BINANCE Perpetual/USDT Crypto"),
45
+ ],
46
+ )
47
+ async def test_get_l1_book_snapshot(async_client: AsyncClient, venue: str, symbol: str):
48
+ snap = await async_client.get_l1_book_snapshot(symbol, venue)
49
+ assert snap is not None
50
+ assert snap.best_bid is not None
51
+ assert snap.best_ask is not None
52
+
53
+
54
+ @pytest.mark.asyncio
55
+ @pytest.mark.parametrize(
56
+ "venue,symbol",
57
+ [
58
+ ("BINANCE/USDM", "BTC-USDT BINANCE Perpetual/USDT Crypto"),
59
+ ],
60
+ )
61
+ async def test_get_l2_book_snapshot(async_client: AsyncClient, venue: str, symbol: str):
62
+ snap = await async_client.get_l2_book_snapshot(symbol, venue)
63
+ assert snap is not None
64
+ assert len(snap.bids) > 0
65
+ assert len(snap.asks) > 0
66
+
67
+
68
+ @pytest.mark.asyncio
69
+ @pytest.mark.parametrize(
70
+ "venue,symbol",
71
+ [
72
+ ("BINANCE/USDM", "BTC-USDT BINANCE Perpetual/USDT Crypto"),
73
+ ],
74
+ )
75
+ async def test_get_ticker(async_client: AsyncClient, venue: str, symbol: str):
76
+ ticker = await async_client.get_ticker(symbol, venue)
77
+ assert ticker is not None
78
+ assert ticker.last_price is not None
79
+
80
+
81
+ @pytest.mark.asyncio
82
+ @pytest.mark.parametrize(
83
+ "venue,symbol",
84
+ [
85
+ ("BINANCE/USDM", "BTC-USDT BINANCE Perpetual/USDT Crypto"),
86
+ ],
87
+ )
88
+ async def test_stream_l1_book_snapshots(
89
+ async_client: AsyncClient, venue: str, symbol: str
90
+ ):
91
+ i = 0
92
+ async for snap in async_client.stream_l1_book_snapshots([symbol], venue):
93
+ assert snap is not None
94
+ assert snap.best_bid is not None
95
+ assert snap.best_ask is not None
96
+ i += 1
97
+ if i > 20:
98
+ break
99
+
100
+
101
+ @pytest.mark.asyncio
102
+ @pytest.mark.parametrize(
103
+ "venue,symbol",
104
+ [
105
+ ("BINANCE/USDM", "BTC-USDT BINANCE Perpetual/USDT Crypto"),
106
+ ],
107
+ )
108
+ async def test_stream_l2_book_updates(
109
+ async_client: AsyncClient, venue: str, symbol: str
110
+ ):
111
+ from architect_py.grpc.models.Marketdata.L2BookUpdate import Diff, Snapshot
112
+
113
+ i = 0
114
+ sid = None
115
+ sn = None
116
+ async for up in async_client.stream_l2_book_updates(symbol, venue):
117
+ assert up is not None
118
+ if isinstance(up, Snapshot):
119
+ assert len(up.bids) > 0
120
+ assert len(up.asks) > 0
121
+ assert i == 0, "snapshot should be the first update"
122
+ sid = up.sequence_id
123
+ sn = up.sequence_number
124
+ elif isinstance(up, Diff):
125
+ assert i > 0, "diff should not be the first update"
126
+ assert sid == up.sequence_id, "sequence number reset on diff"
127
+ assert sn is not None, "sequence number should be set"
128
+ assert up.sequence_number == sn + 1, (
129
+ "sequence number not monotonically increasing"
130
+ )
131
+ sn = up.sequence_number
132
+ i += 1
133
+ if i > 20:
134
+ break
135
+
136
+
137
+ @pytest.mark.asyncio
138
+ @pytest.mark.parametrize(
139
+ "venue,symbol",
140
+ [
141
+ ("BINANCE/USDM", "BTC-USDT BINANCE Perpetual/USDT Crypto"),
142
+ ],
143
+ )
144
+ async def test_stream_trades(async_client: AsyncClient, venue: str, symbol: str):
145
+ i = 0
146
+ async for trade in async_client.stream_trades(symbol, venue):
147
+ assert trade is not None
148
+ i += 1
149
+ if i > 20:
150
+ break
151
+
152
+
153
+ # @pytest.mark.asyncio
154
+ # @pytest.mark.parametrize(
155
+ # "venue,symbol",
156
+ # [
157
+ # ("BINANCE/USDM", "BTC-USDT BINANCE Perpetual/USDT Crypto"),
158
+ # ],
159
+ # )
160
+ # async def test_stream_candles(async_client: AsyncClient, venue: str, symbol: str):
161
+ # i = 0
162
+ # async for candle in async_client.stream_candles(
163
+ # symbol, venue, [CandleWidth.OneSecond]
164
+ # ):
165
+ # assert candle is not None
166
+ # i += 1
167
+ # if i > 3:
168
+ # break
@@ -0,0 +1,37 @@
1
+ import asyncio
2
+ from decimal import Decimal
3
+
4
+ import pytest
5
+
6
+ from architect_py import AsyncClient, OrderDir
7
+ from architect_py.utils.nearest_tick_2 import TickRoundMethod
8
+
9
+
10
+ @pytest.mark.asyncio
11
+ @pytest.mark.timeout(3)
12
+ async def test_place_limit_order(async_client: AsyncClient):
13
+ # CR alee: there's no good way to get the front month future
14
+ symbol = "MET 20250530 CME Future/USD"
15
+ venue = "CME"
16
+ info = await async_client.get_execution_info(symbol, venue)
17
+ assert info is not None
18
+ assert info.tick_size is not None
19
+ snap = await async_client.get_ticker(symbol, venue)
20
+ assert snap is not None
21
+ assert snap.bid_price is not None
22
+ accounts = await async_client.list_accounts()
23
+ account = accounts[0]
24
+
25
+ # bid far below the best bid
26
+ limit_price = TickRoundMethod.FLOOR(snap.bid_price * Decimal(0.9), info.tick_size)
27
+ order = await async_client.place_limit_order(
28
+ symbol=symbol,
29
+ execution_venue=venue,
30
+ odir=OrderDir.BUY,
31
+ quantity=Decimal(1),
32
+ limit_price=limit_price,
33
+ account=str(account.account.id),
34
+ )
35
+ assert order is not None
36
+ await asyncio.sleep(1)
37
+ await async_client.cancel_order(order.id)