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.
- architect_py/__init__.py +18 -2
- architect_py/async_client.py +1089 -658
- architect_py/client.py +36 -33
- 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 +145 -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 +5 -4
- architect_py/{grpc_client → grpc/models}/Cpty/CptyResponse.py +6 -6
- architect_py/grpc/models/Cpty/CptyStatus.py +48 -0
- architect_py/grpc/models/Cpty/CptyStatusRequest.py +45 -0
- architect_py/grpc/models/Cpty/CptysRequest.py +37 -0
- architect_py/grpc/models/Cpty/CptysResponse.py +27 -0
- 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 +14 -3
- 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 +2 -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 +671 -934
- architect_py/grpc/resolve_endpoint.py +70 -0
- architect_py/{grpc_client/grpc_server.py → grpc/server.py} +13 -9
- 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 -85
- 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.0.dist-info/METADATA +54 -0
- architect_py-5.0.0.dist-info/RECORD +214 -0
- {architect_py-3.2.1.dist-info → architect_py-5.0.0.dist-info}/WHEEL +2 -1
- architect_py-5.0.0.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 -52
- 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/Folio/AggregatedAccountSummariesRequest.py +0 -59
- architect_py/grpc_client/Folio/AggregatedAccountSummariesResponse.py +0 -27
- 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 -405
- 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 -61
- architect_py/tests/test_snapshots.py +0 -52
- architect_py/tests/test_subscriptions.py +0 -129
- architect_py-3.2.1.dist-info/METADATA +0 -212
- architect_py-3.2.1.dist-info/RECORD +0 -146
- /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.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
|
-
|
4
|
-
from .
|
5
|
-
from .
|
6
|
-
from .
|
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
|
-
|
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
|
-
|
41
|
-
|
42
|
-
|
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
|
architect_py/tests/conftest.py
CHANGED
@@ -1,118 +1,124 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
"""
|
2
|
+
Tests for architect-py can be run against any particular environment.
|
3
3
|
|
4
|
-
|
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
|
-
|
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
|
-
|
13
|
+
- ARCHITECT_GRAPHQL_PORT (default=4567)
|
14
|
+
- DANGEROUS_ALLOW_LIVE_TRADING (default=False)
|
15
15
|
|
16
|
-
|
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
|
-
|
23
|
-
|
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
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
39
|
+
assert endpoint is not None
|
40
|
+
assert api_key is not None
|
41
|
+
assert api_secret is not None
|
44
42
|
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
-
@
|
64
|
-
def
|
80
|
+
@pytest_asyncio.fixture
|
81
|
+
async def async_client() -> AsyncClient:
|
65
82
|
load_dotenv()
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
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
|
-
|
72
|
-
|
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
|
-
|
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
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
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
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
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
|
112
|
-
|
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
|
-
|
124
|
+
# CR alee: add sync Client tests
|
@@ -1,56 +1,55 @@
|
|
1
|
-
|
2
|
-
import
|
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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
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)
|