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.
- architect_py/__init__.py +15 -2
- architect_py/async_client.py +1060 -643
- architect_py/client.py +25 -26
- architect_py/client_interface.py +63 -0
- architect_py/common_types/__init__.py +6 -0
- architect_py/common_types/order_dir.py +91 -0
- architect_py/common_types/scalars.py +25 -0
- architect_py/common_types/tradable_product.py +59 -0
- architect_py/graphql_client/__init__.py +2 -0
- architect_py/graphql_client/client.py +3 -6
- architect_py/graphql_client/enums.py +5 -0
- architect_py/graphql_client/fragments.py +3 -6
- architect_py/graphql_client/get_fills_query.py +2 -1
- architect_py/graphql_client/search_symbols_query.py +2 -1
- architect_py/graphql_client/subscribe_orderflow.py +2 -1
- architect_py/graphql_client/subscribe_trades.py +2 -1
- architect_py/grpc/__init__.py +143 -0
- architect_py/grpc/client.py +94 -0
- architect_py/{grpc_client → grpc/models}/Accounts/AccountsRequest.py +6 -3
- architect_py/{grpc_client → grpc/models}/Accounts/AccountsResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Accounts/__init__.py +1 -1
- architect_py/grpc/models/Algo/AlgoOrder.py +114 -0
- architect_py/grpc/models/Algo/AlgoOrderRequest.py +46 -0
- architect_py/grpc/models/Algo/AlgoOrdersRequest.py +72 -0
- architect_py/grpc/models/Algo/AlgoOrdersResponse.py +27 -0
- architect_py/grpc/models/Algo/CreateAlgoOrderRequest.py +56 -0
- architect_py/grpc/models/Algo/PauseAlgoRequest.py +42 -0
- architect_py/grpc/models/Algo/PauseAlgoResponse.py +20 -0
- architect_py/grpc/models/Algo/StartAlgoRequest.py +42 -0
- architect_py/grpc/models/Algo/StartAlgoResponse.py +20 -0
- architect_py/grpc/models/Algo/StopAlgoRequest.py +42 -0
- architect_py/grpc/models/Algo/StopAlgoResponse.py +20 -0
- architect_py/{grpc_client → grpc/models}/Algo/__init__.py +1 -1
- architect_py/grpc/models/Auth/CreateJwtRequest.py +47 -0
- architect_py/grpc/models/Auth/CreateJwtResponse.py +23 -0
- architect_py/{grpc_client/Cpty → grpc/models/Auth}/__init__.py +1 -1
- architect_py/grpc/models/Boss/DepositsRequest.py +40 -0
- architect_py/grpc/models/Boss/DepositsResponse.py +27 -0
- architect_py/grpc/models/Boss/RqdAccountStatisticsRequest.py +42 -0
- architect_py/grpc/models/Boss/RqdAccountStatisticsResponse.py +25 -0
- architect_py/grpc/models/Boss/StatementUrlRequest.py +40 -0
- architect_py/grpc/models/Boss/StatementUrlResponse.py +23 -0
- architect_py/grpc/models/Boss/StatementsRequest.py +40 -0
- architect_py/grpc/models/Boss/StatementsResponse.py +27 -0
- architect_py/grpc/models/Boss/WithdrawalsRequest.py +40 -0
- architect_py/grpc/models/Boss/WithdrawalsResponse.py +27 -0
- architect_py/{grpc_client/Folio → grpc/models/Boss}/__init__.py +1 -1
- architect_py/grpc/models/Core/ConfigRequest.py +37 -0
- architect_py/grpc/models/Core/ConfigResponse.py +25 -0
- architect_py/grpc/models/Core/__init__.py +2 -0
- architect_py/{grpc_client → grpc/models}/Cpty/CptyRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Cpty/CptyResponse.py +3 -3
- architect_py/{grpc_client → grpc/models}/Cpty/CptyStatus.py +1 -1
- architect_py/{grpc_client → grpc/models}/Cpty/CptyStatusRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Cpty/CptysRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Cpty/CptysResponse.py +1 -1
- architect_py/grpc/models/Cpty/__init__.py +2 -0
- architect_py/{grpc_client → grpc/models}/Folio/AccountHistoryRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Folio/AccountHistoryResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Folio/AccountSummariesRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Folio/AccountSummariesResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Folio/AccountSummary.py +1 -1
- architect_py/{grpc_client → grpc/models}/Folio/AccountSummaryRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Folio/HistoricalFillsRequest.py +7 -4
- architect_py/{grpc_client → grpc/models}/Folio/HistoricalFillsResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Folio/HistoricalOrdersRequest.py +3 -3
- architect_py/{grpc_client → grpc/models}/Folio/HistoricalOrdersResponse.py +1 -1
- architect_py/grpc/models/Folio/__init__.py +2 -0
- architect_py/{grpc_client → grpc/models}/Health/HealthCheckRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Health/HealthCheckResponse.py +1 -1
- architect_py/grpc/models/Health/__init__.py +2 -0
- architect_py/{grpc_client → grpc/models}/Marketdata/Candle.py +1 -1
- architect_py/{grpc_client → grpc/models}/Marketdata/HistoricalCandlesRequest.py +11 -8
- architect_py/{grpc_client → grpc/models}/Marketdata/HistoricalCandlesResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Marketdata/L1BookSnapshot.py +52 -5
- architect_py/{grpc_client → grpc/models}/Marketdata/L1BookSnapshotRequest.py +8 -3
- architect_py/{grpc_client → grpc/models}/Marketdata/L1BookSnapshotsRequest.py +6 -3
- architect_py/{grpc_client → grpc/models}/Marketdata/L2BookSnapshot.py +1 -1
- architect_py/{grpc_client → grpc/models}/Marketdata/L2BookSnapshotRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/Liquidation.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/MarketStatus.py +1 -1
- architect_py/{grpc_client → grpc/models}/Marketdata/MarketStatusRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeCandlesRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeCurrentCandlesRequest.py +3 -4
- architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeL1BookSnapshotsRequest.py +6 -3
- architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeL2BookUpdatesRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeLiquidationsRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeManyCandlesRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeTickersRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeTradesRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/Ticker.py +1 -1
- architect_py/{grpc_client → grpc/models}/Marketdata/TickerRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/TickersRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/TickersResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Marketdata/Trade.py +2 -2
- architect_py/grpc/models/Marketdata/__init__.py +2 -0
- architect_py/grpc/models/Oms/Cancel.py +90 -0
- architect_py/{grpc_client → grpc/models}/Oms/CancelAllOrdersRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Oms/CancelAllOrdersResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Oms/CancelOrderRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Oms/OpenOrdersRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Oms/OpenOrdersResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Oms/Order.py +6 -13
- architect_py/{grpc_client → grpc/models}/Oms/PendingCancelsRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Oms/PendingCancelsResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Oms/PlaceOrderRequest.py +16 -23
- architect_py/grpc/models/Oms/__init__.py +2 -0
- architect_py/grpc/models/OptionsMarketdata/OptionsChain.py +30 -0
- architect_py/grpc/models/OptionsMarketdata/OptionsChainGreeks.py +30 -0
- architect_py/grpc/models/OptionsMarketdata/OptionsChainGreeksRequest.py +47 -0
- architect_py/grpc/models/OptionsMarketdata/OptionsChainRequest.py +45 -0
- architect_py/grpc/models/OptionsMarketdata/OptionsExpirations.py +29 -0
- architect_py/grpc/models/OptionsMarketdata/OptionsExpirationsRequest.py +42 -0
- architect_py/grpc/models/OptionsMarketdata/__init__.py +2 -0
- architect_py/{grpc_client → grpc/models}/Orderflow/DropcopyRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Orderflow/OrderflowRequest.py +1 -1
- architect_py/{grpc_client → grpc/models}/Orderflow/SubscribeOrderflowRequest.py +2 -2
- architect_py/grpc/models/Orderflow/__init__.py +2 -0
- architect_py/grpc/models/Symbology/DownloadProductCatalogRequest.py +42 -0
- architect_py/grpc/models/Symbology/DownloadProductCatalogResponse.py +27 -0
- architect_py/grpc/models/Symbology/ExecutionInfoRequest.py +47 -0
- architect_py/grpc/models/Symbology/ExecutionInfoResponse.py +27 -0
- architect_py/{grpc_client → grpc/models}/Symbology/PruneExpiredSymbolsRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Symbology/PruneExpiredSymbolsResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Symbology/SubscribeSymbology.py +1 -1
- architect_py/{grpc_client → grpc/models}/Symbology/SymbologyRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Symbology/SymbologySnapshot.py +7 -2
- architect_py/{grpc_client → grpc/models}/Symbology/SymbologyUpdate.py +9 -2
- architect_py/{grpc_client → grpc/models}/Symbology/SymbolsRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Symbology/SymbolsResponse.py +1 -1
- architect_py/grpc/models/Symbology/UploadProductCatalogRequest.py +49 -0
- architect_py/grpc/models/Symbology/UploadProductCatalogResponse.py +20 -0
- architect_py/{grpc_client → grpc/models}/Symbology/UploadSymbologyRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Symbology/UploadSymbologyResponse.py +1 -1
- architect_py/grpc/models/Symbology/__init__.py +2 -0
- architect_py/grpc/models/__init__.py +2 -0
- architect_py/{grpc_client → grpc/models}/definitions.py +690 -841
- architect_py/grpc/resolve_endpoint.py +70 -0
- architect_py/{grpc_client/grpc_server.py → grpc/server.py} +9 -6
- architect_py/grpc/utils.py +32 -0
- architect_py/internal_utils/__init__.py +0 -0
- architect_py/internal_utils/no_pandas.py +3 -0
- architect_py/tests/conftest.py +91 -87
- architect_py/tests/test_book_building.py +49 -50
- architect_py/tests/test_marketdata.py +168 -0
- architect_py/tests/test_order_entry.py +37 -0
- architect_py/tests/test_orderflow.py +41 -0
- architect_py/tests/test_portfolio_management.py +23 -0
- architect_py/tests/test_rounding.py +28 -28
- architect_py/tests/test_symbology.py +37 -30
- architect_py/utils/nearest_tick.py +2 -5
- architect_py/utils/nearest_tick_2.py +1 -2
- architect_py/utils/orderbook.py +35 -0
- architect_py/utils/pandas.py +44 -0
- architect_py/utils/price_bands.py +0 -3
- architect_py/utils/symbol_parsing.py +29 -0
- architect_py-5.0.0b2.dist-info/METADATA +123 -0
- architect_py-5.0.0b2.dist-info/RECORD +214 -0
- {architect_py-3.2.2.dist-info → architect_py-5.0.0b2.dist-info}/WHEEL +2 -1
- architect_py-5.0.0b2.dist-info/top_level.txt +4 -0
- examples/__init__.py +0 -0
- examples/book_subscription.py +52 -0
- examples/candles.py +30 -0
- examples/common.py +116 -0
- examples/external_cpty.py +77 -0
- examples/funding_rate_mean_reversion_algo.py +186 -0
- examples/order_sending.py +91 -0
- examples/stream_l1_marketdata.py +25 -0
- examples/stream_l2_marketdata.py +38 -0
- examples/trades.py +21 -0
- examples/tutorial_async.py +86 -0
- examples/tutorial_sync.py +95 -0
- scripts/generate_functions_md.py +166 -0
- scripts/generate_sync_interface.py +226 -0
- scripts/postprocess_grpc.py +604 -0
- scripts/preprocess_grpc_schema.py +708 -0
- templates/exceptions.py +83 -0
- templates/juniper_base_client.py +371 -0
- architect_py/client_protocol.py +0 -53
- architect_py/grpc_client/Algo/AlgoOrderForTwapAlgo.py +0 -61
- architect_py/grpc_client/Algo/CreateAlgoOrderRequestForTwapAlgo.py +0 -59
- architect_py/grpc_client/Algo/ModifyAlgoOrderRequestForTwapAlgo.py +0 -45
- architect_py/grpc_client/Health/__init__.py +0 -2
- architect_py/grpc_client/Marketdata/__init__.py +0 -2
- architect_py/grpc_client/Oms/Cancel.py +0 -42
- architect_py/grpc_client/Oms/__init__.py +0 -2
- architect_py/grpc_client/Orderflow/__init__.py +0 -2
- architect_py/grpc_client/Symbology/__init__.py +0 -2
- architect_py/grpc_client/__init__.py +0 -2
- architect_py/grpc_client/grpc_client.py +0 -413
- architect_py/scalars.py +0 -172
- architect_py/tests/test_accounts.py +0 -31
- architect_py/tests/test_client.py +0 -29
- architect_py/tests/test_grpc_client.py +0 -30
- architect_py/tests/test_order_sending.py +0 -65
- architect_py/tests/test_snapshots.py +0 -52
- architect_py/tests/test_subscriptions.py +0 -126
- architect_py-3.2.2.dist-info/METADATA +0 -191
- architect_py-3.2.2.dist-info/RECORD +0 -148
- /architect_py/{grpc_client → grpc/models}/Marketdata/ArrayOfL1BookSnapshot.py +0 -0
- /architect_py/{grpc_client → grpc/models}/Marketdata/L2BookUpdate.py +0 -0
- /architect_py/{grpc_client → grpc/models}/Marketdata/TickerUpdate.py +0 -0
- /architect_py/{grpc_client → grpc/models}/Orderflow/Dropcopy.py +0 -0
- /architect_py/{grpc_client → grpc/models}/Orderflow/Orderflow.py +0 -0
- {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,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
|