architect-py 3.2.2__py3-none-any.whl → 5.0.0b1__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 +8 -2
- architect_py/async_client.py +879 -576
- architect_py/client.py +25 -26
- architect_py/client_interface.py +62 -0
- architect_py/common_types/__init__.py +6 -0
- architect_py/common_types/order_dir.py +85 -0
- architect_py/common_types/scalars.py +25 -0
- architect_py/common_types/tradable_product.py +59 -0
- architect_py/graphql_client/client.py +3 -6
- 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 +125 -0
- architect_py/grpc/client.py +86 -0
- architect_py/{grpc_client → grpc/models}/Accounts/AccountsRequest.py +2 -2
- 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_client → grpc/models}/Algo/AlgoOrderForTwapAlgo.py +1 -1
- architect_py/{grpc_client → grpc/models}/Algo/CreateAlgoOrderRequestForTwapAlgo.py +2 -2
- architect_py/{grpc_client → grpc/models}/Algo/ModifyAlgoOrderRequestForTwapAlgo.py +2 -2
- 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/Core/ConfigRequest.py +37 -0
- architect_py/grpc/models/Core/ConfigResponse.py +25 -0
- architect_py/{grpc_client/Folio → grpc/models/Core}/__init__.py +1 -1
- 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 +3 -3
- 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 +36 -3
- 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_client → grpc/models}/Oms/Cancel.py +1 -1
- 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 +2 -2
- 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 +3 -3
- architect_py/grpc/models/Oms/__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_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 +248 -66
- architect_py/grpc/resolve_endpoint.py +67 -0
- architect_py/{grpc_client/grpc_server.py → grpc/server.py} +9 -6
- architect_py/grpc/utils.py +32 -0
- architect_py/tests/conftest.py +86 -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 +38 -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.0b1.dist-info/METADATA +124 -0
- architect_py-5.0.0b1.dist-info/RECORD +184 -0
- {architect_py-3.2.2.dist-info → architect_py-5.0.0b1.dist-info}/WHEEL +2 -1
- architect_py-5.0.0b1.dist-info/top_level.txt +4 -0
- examples/__init__.py +0 -0
- examples/book_subscription.py +53 -0
- examples/candles.py +30 -0
- examples/common.py +107 -0
- examples/external_cpty.py +77 -0
- examples/funding_rate_mean_reversion_algo.py +192 -0
- examples/order_sending.py +92 -0
- examples/stream_l1_marketdata.py +25 -0
- examples/stream_l2_marketdata.py +40 -0
- examples/trades.py +21 -0
- examples/tutorial_async.py +84 -0
- examples/tutorial_sync.py +95 -0
- scripts/generate_functions_md.py +164 -0
- scripts/generate_sync_interface.py +207 -0
- scripts/postprocess_grpc.py +594 -0
- scripts/preprocess_grpc_schema.py +647 -0
- templates/exceptions.py +83 -0
- templates/juniper_base_client.py +371 -0
- architect_py/client_protocol.py +0 -53
- architect_py/grpc_client/Health/__init__.py +0 -2
- architect_py/grpc_client/Marketdata/__init__.py +0 -2
- 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.0b1.dist-info/licenses}/LICENSE +0 -0
architect_py/async_client.py
CHANGED
@@ -1,216 +1,345 @@
|
|
1
|
-
"""
|
2
|
-
This file composes the GraphQLClient class to provide a higher-level interface
|
3
|
-
for order entry with the Architect API.
|
4
|
-
|
5
|
-
These are not required to send orders, but provide typed interfaces for the
|
6
|
-
various order types and algorithms that can be sent to the OMS.
|
7
|
-
|
8
|
-
|
9
|
-
The functions to send orders will return the order ID string
|
10
|
-
After sending the order, this string can be used to retrieve the order status
|
11
|
-
|
12
|
-
send_limit_order -> get_order
|
13
|
-
|
14
|
-
The individual graphql types are subject to change, so it is not recommended to use them directly.
|
15
|
-
"""
|
16
|
-
|
17
1
|
import asyncio
|
18
|
-
import functools
|
19
2
|
import logging
|
20
|
-
|
3
|
+
import re
|
4
|
+
from datetime import date, datetime, timedelta
|
21
5
|
from decimal import Decimal
|
22
|
-
from typing import
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
6
|
+
from typing import (
|
7
|
+
AsyncIterator,
|
8
|
+
List,
|
9
|
+
Literal,
|
10
|
+
Optional,
|
11
|
+
Sequence,
|
12
|
+
Union,
|
13
|
+
overload,
|
27
14
|
)
|
28
15
|
|
29
|
-
from architect_py.
|
30
|
-
from architect_py.
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
HistoricalCandlesResponse,
|
16
|
+
from architect_py.grpc.models.Orderflow.Orderflow import Orderflow
|
17
|
+
from architect_py.grpc.models.Orderflow.OrderflowRequest import (
|
18
|
+
OrderflowRequest,
|
19
|
+
OrderflowRequest_route,
|
20
|
+
OrderflowRequestUnannotatedResponseType,
|
35
21
|
)
|
36
|
-
|
37
|
-
|
38
|
-
from architect_py.grpc_client.Marketdata.L1BookSnapshot import L1BookSnapshot
|
39
|
-
from architect_py.grpc_client.Marketdata.L2BookSnapshot import L2BookSnapshot
|
40
|
-
from architect_py.grpc_client.Marketdata.L2BookUpdate import L2BookUpdate
|
41
|
-
from architect_py.grpc_client.Marketdata.SubscribeCandlesRequest import (
|
42
|
-
SubscribeCandlesRequest,
|
22
|
+
from architect_py.grpc.models.Orderflow.SubscribeOrderflowRequest import (
|
23
|
+
SubscribeOrderflowRequest,
|
43
24
|
)
|
44
|
-
from architect_py.grpc_client.Marketdata.SubscribeTradesRequest import (
|
45
|
-
SubscribeTradesRequest,
|
46
|
-
)
|
47
|
-
from architect_py.grpc_client.Marketdata.Trade import Trade
|
48
|
-
from architect_py.scalars import OrderDir, TradableProduct
|
49
|
-
from architect_py.utils.nearest_tick import TickRoundMethod
|
50
25
|
|
26
|
+
from .common_types import OrderDir, TradableProduct, Venue
|
51
27
|
from .graphql_client import GraphQLClient
|
52
28
|
from .graphql_client.enums import (
|
53
29
|
OrderType,
|
54
30
|
TimeInForce,
|
55
31
|
)
|
32
|
+
from .graphql_client.exceptions import GraphQLClientGraphQLMultiError
|
56
33
|
from .graphql_client.fragments import (
|
57
34
|
AccountSummaryFields,
|
58
35
|
AccountWithPermissionsFields,
|
59
36
|
CancelFields,
|
60
37
|
ExecutionInfoFields,
|
61
|
-
L2BookFields,
|
62
|
-
MarketStatusFields,
|
63
|
-
MarketTickerFields,
|
64
38
|
OrderFields,
|
65
39
|
ProductInfoFields,
|
66
40
|
)
|
41
|
+
from .graphql_client.get_fills_query import GetFillsQueryFolioHistoricalFills
|
42
|
+
from .graphql_client.place_order_mutation import PlaceOrderMutationOms
|
43
|
+
from .grpc import *
|
44
|
+
from .grpc.client import GrpcClient
|
45
|
+
from .grpc.models import definitions as grpc_definitions
|
46
|
+
from .utils.nearest_tick import TickRoundMethod
|
47
|
+
from .utils.orderbook import update_orderbook_side
|
48
|
+
from .utils.price_bands import price_band_pairs
|
49
|
+
from .utils.symbol_parsing import nominative_expiration
|
67
50
|
|
68
|
-
|
69
|
-
|
70
|
-
# CreateOrder,
|
71
|
-
# CreatePovAlgo,
|
72
|
-
# CreateSmartOrderRouterAlgo,
|
73
|
-
# CreateSpreadAlgo,
|
74
|
-
# CreateSpreadAlgoHedgeMarket,
|
75
|
-
# CreateTimeInForce,
|
76
|
-
# CreateTimeInForceInstruction,
|
77
|
-
# CreateTwapAlgo,
|
78
|
-
# )
|
79
|
-
from .grpc_client import GRPCClient
|
51
|
+
try:
|
52
|
+
import pandas as pd
|
80
53
|
|
81
|
-
from .utils.
|
54
|
+
from .utils.pandas import candles_to_dataframe
|
82
55
|
|
83
|
-
|
56
|
+
FEATURE_PANDAS = True
|
57
|
+
except ImportError:
|
58
|
+
FEATURE_PANDAS = False
|
84
59
|
|
85
60
|
|
86
61
|
class AsyncClient:
|
62
|
+
api_key: Optional[str] = None
|
63
|
+
api_secret: Optional[str] = None
|
64
|
+
paper_trading: bool
|
87
65
|
graphql_client: GraphQLClient
|
88
|
-
|
66
|
+
grpc_core: Optional[GrpcClient] = None
|
67
|
+
grpc_marketdata: dict[Venue, GrpcClient] = {}
|
68
|
+
grpc_hmart: Optional[GrpcClient] = None
|
69
|
+
jwt: str | None = None
|
70
|
+
jwt_expiration: datetime | None = None
|
71
|
+
|
72
|
+
l1_books: dict[Venue, dict[TradableProduct, tuple[L1BookSnapshot, asyncio.Task]]]
|
73
|
+
l2_books: dict[Venue, dict[TradableProduct, tuple[L2BookSnapshot, asyncio.Task]]]
|
89
74
|
|
90
75
|
# ------------------------------------------------------------
|
91
|
-
# Initialization
|
76
|
+
# Initialization and configuration
|
92
77
|
# ------------------------------------------------------------
|
93
78
|
|
94
79
|
@staticmethod
|
95
80
|
async def connect(
|
96
81
|
*,
|
97
|
-
api_key: str,
|
98
|
-
api_secret: str,
|
82
|
+
api_key: Optional[str] = None,
|
83
|
+
api_secret: Optional[str] = None,
|
99
84
|
paper_trading: bool,
|
100
|
-
|
101
|
-
|
102
|
-
_port: Optional[int] = None,
|
103
|
-
**kwargs: Any,
|
85
|
+
endpoint: str = "https://app.architect.co",
|
86
|
+
graphql_port: Optional[int] = None,
|
104
87
|
) -> "AsyncClient":
|
105
88
|
"""
|
106
|
-
|
107
|
-
|
108
|
-
Args:
|
109
|
-
api_key: API key for the user
|
110
|
-
api_secret: API secret for the user
|
111
|
-
host: Host for the GraphQL server, defaults to "app.architect.co"
|
112
|
-
paper_trading: Whether to use the paper trading environment, defaults to True
|
113
|
-
_port: Port for the GraphQL server, more for debugging purposes, do not set this unless you are sure of the port
|
114
|
-
|
115
|
-
the API key and secret can be generated on the app.architect.co website
|
89
|
+
Connect to an Architect installation.
|
116
90
|
|
117
|
-
|
118
|
-
Client object
|
119
|
-
|
120
|
-
Raises:
|
121
|
-
ValueError: If the API key or secret are not the correct length or contain invalid characters
|
122
|
-
|
123
|
-
For any request, if you get a "GraphQLClientHttpError: HTTP status code: 500" it likely means that your
|
124
|
-
API key and secret are incorrect. Please double check your credentials.
|
125
|
-
|
126
|
-
If you get a "GraphQLClientHttpError: HTTP status code: 400", please contact support so we can fix the function.
|
127
|
-
|
128
|
-
If you get an AttributeError on the grpc_client, it means that the GRPC client has not been initialized
|
129
|
-
likely due to the client not being instantiated with the connect method
|
91
|
+
Raises ValueError if the API key and secret are not the correct length or contain invalid characters.
|
130
92
|
"""
|
131
93
|
if paper_trading:
|
132
|
-
|
133
|
-
|
134
|
-
)
|
94
|
+
COLOR = "\033[30;43m"
|
95
|
+
RESET = "\033[0m"
|
96
|
+
print(f"🧻 {COLOR} YOU ARE IN PAPER TRADING MODE {RESET}")
|
97
|
+
|
98
|
+
grpc_host, grpc_port, use_ssl = await resolve_endpoint(endpoint)
|
99
|
+
logging.info(
|
100
|
+
f"Resolved endpoint {endpoint}: {grpc_host}:{grpc_port} use_ssl={use_ssl}"
|
101
|
+
)
|
135
102
|
|
136
|
-
|
103
|
+
client = AsyncClient(
|
137
104
|
api_key=api_key,
|
138
105
|
api_secret=api_secret,
|
139
|
-
host=host,
|
140
106
|
paper_trading=paper_trading,
|
141
|
-
|
107
|
+
grpc_host=grpc_host,
|
108
|
+
grpc_port=grpc_port,
|
109
|
+
graphql_port=graphql_port,
|
110
|
+
use_ssl=use_ssl,
|
142
111
|
_i_know_what_i_am_doing=True,
|
143
|
-
**kwargs,
|
144
112
|
)
|
145
113
|
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
114
|
+
logging.info("Exchanging credentials...")
|
115
|
+
await client.refresh_jwt()
|
116
|
+
|
117
|
+
logging.info("Discovering marketdata endpoints...")
|
118
|
+
await client.discover_marketdata()
|
119
|
+
|
120
|
+
return client
|
151
121
|
|
152
122
|
def __init__(
|
153
123
|
self,
|
154
124
|
*,
|
155
|
-
api_key: str,
|
156
|
-
api_secret: str,
|
157
|
-
|
158
|
-
|
159
|
-
|
125
|
+
api_key: Optional[str] = None,
|
126
|
+
api_secret: Optional[str] = None,
|
127
|
+
paper_trading: bool,
|
128
|
+
grpc_host: str = "app.architect.co",
|
129
|
+
grpc_port: int,
|
130
|
+
graphql_port: Optional[int] = None,
|
131
|
+
use_ssl: bool = True,
|
160
132
|
_i_know_what_i_am_doing: bool = False,
|
161
|
-
**kwargs: Any,
|
162
133
|
):
|
163
134
|
"""
|
164
|
-
|
165
|
-
|
166
|
-
Use the connect method instead.
|
167
|
-
See self.connect for arg explanations
|
135
|
+
Use AsyncClient.connect instead.
|
168
136
|
"""
|
169
|
-
|
170
137
|
if not _i_know_what_i_am_doing:
|
171
|
-
raise ValueError(
|
172
|
-
"Please use the connect method to create an AsyncClient object."
|
173
|
-
)
|
138
|
+
raise ValueError("Use AsyncClient.connect to create an AsyncClient object.")
|
174
139
|
|
175
|
-
if not
|
176
|
-
raise ValueError(
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
raise ValueError(
|
181
|
-
"API key and secret cannot contain commas, please double check your credentials."
|
182
|
-
)
|
183
|
-
elif " " in api_key or " " in api_secret:
|
140
|
+
if api_key is not None and not re.match(r"^[a-zA-Z0-9]{24}$", api_key):
|
141
|
+
raise ValueError("API key must be exactly 24 alphanumeric characters")
|
142
|
+
if api_secret is not None and not re.match(
|
143
|
+
r"^[a-zA-Z0-9+\/=]{44}$", api_secret
|
144
|
+
):
|
184
145
|
raise ValueError(
|
185
|
-
"API
|
146
|
+
"API secret must be a Base64-encoded string, 44 characters long"
|
186
147
|
)
|
187
|
-
|
148
|
+
|
149
|
+
if paper_trading and (graphql_port is not None or grpc_port is not None):
|
188
150
|
raise ValueError(
|
189
|
-
"
|
151
|
+
"If paper_trading is True, graphql_port and grpc_port must be None"
|
190
152
|
)
|
191
153
|
|
192
|
-
if
|
154
|
+
if graphql_port is None:
|
193
155
|
if paper_trading:
|
194
|
-
|
156
|
+
graphql_port = 5678
|
195
157
|
else:
|
196
|
-
|
158
|
+
graphql_port = 4567
|
197
159
|
|
160
|
+
self.api_key = api_key
|
161
|
+
self.api_secret = api_secret
|
162
|
+
self.paper_trading = paper_trading
|
198
163
|
self.graphql_client = GraphQLClient(
|
199
|
-
|
164
|
+
host=grpc_host,
|
165
|
+
port=graphql_port,
|
166
|
+
use_tls=use_ssl,
|
167
|
+
api_key=api_key,
|
168
|
+
api_secret=api_secret,
|
200
169
|
)
|
170
|
+
self.grpc_core = GrpcClient(host=grpc_host, port=grpc_port, use_ssl=use_ssl)
|
171
|
+
|
172
|
+
async def refresh_jwt(self, force: bool = False):
|
173
|
+
"""
|
174
|
+
Refresh the JWT for the gRPC channel if it's nearing expiration (within 1 minute).
|
175
|
+
If force=True, refresh the JWT unconditionally.
|
176
|
+
|
177
|
+
Query methods on AsyncClient that require auth will call this method internally.
|
178
|
+
"""
|
179
|
+
if not self.api_key or not self.api_secret:
|
180
|
+
raise ValueError("API key and secret not set")
|
181
|
+
if self.grpc_core is None:
|
182
|
+
raise ValueError("gRPC client to Architect not initialized")
|
183
|
+
|
184
|
+
if (
|
185
|
+
force
|
186
|
+
or self.jwt_expiration is None
|
187
|
+
or datetime.now() > self.jwt_expiration - timedelta(minutes=1)
|
188
|
+
):
|
189
|
+
try:
|
190
|
+
req = CreateJwtRequest(api_key=self.api_key, api_secret=self.api_secret)
|
191
|
+
res: CreateJwtResponse = await self.grpc_core.unary_unary(req)
|
192
|
+
self.jwt = res.jwt
|
193
|
+
# CR alee: actually read the JWT to get the expiration time;
|
194
|
+
# for now, we just "know" that the JWTs are granted for an hour
|
195
|
+
self.jwt_expiration = datetime.now() + timedelta(hours=1)
|
196
|
+
except Exception as e:
|
197
|
+
logging.error("Failed to refresh gRPC credentials: %s", e)
|
198
|
+
|
199
|
+
def set_jwt(self, jwt: str | None, jwt_expiration: datetime | None = None):
|
200
|
+
"""
|
201
|
+
Manually set the JWT for gRPC authentication.
|
202
|
+
|
203
|
+
Args:
|
204
|
+
jwt: the JWT to set;
|
205
|
+
None to clear the JWT
|
206
|
+
jwt_expiration: when to expire the JWT
|
207
|
+
"""
|
208
|
+
self.jwt = jwt
|
209
|
+
self.jwt_expiration = jwt_expiration
|
201
210
|
|
202
|
-
async def
|
211
|
+
async def discover_marketdata(self):
|
203
212
|
"""
|
204
|
-
Load
|
213
|
+
Load marketdata endpoints from the server config.
|
205
214
|
|
206
|
-
|
215
|
+
The Architect core is responsible for telling you where to find marketdata as per
|
216
|
+
its configuration. You can also manually set marketdata endpoints by calling
|
217
|
+
set_marketdata directly.
|
218
|
+
|
219
|
+
This method is called on AsyncClient.connect.
|
207
220
|
"""
|
208
|
-
|
221
|
+
try:
|
222
|
+
grpc_client = await self.core()
|
223
|
+
req = ConfigRequest()
|
224
|
+
res: ConfigResponse = await grpc_client.unary_unary(req)
|
225
|
+
for venue, endpoint in res.marketdata.items():
|
226
|
+
try:
|
227
|
+
grpc_host, grpc_port, use_ssl = await resolve_endpoint(endpoint)
|
228
|
+
logging.info(
|
229
|
+
"Setting marketdata endpoint for %s: %s:%d use_ssl=%s",
|
230
|
+
venue,
|
231
|
+
grpc_host,
|
232
|
+
grpc_port,
|
233
|
+
use_ssl,
|
234
|
+
)
|
235
|
+
self.grpc_marketdata[venue] = GrpcClient(
|
236
|
+
host=grpc_host, port=grpc_port, use_ssl=use_ssl
|
237
|
+
)
|
238
|
+
except Exception as e:
|
239
|
+
logging.error("Failed to set marketdata endpoint: %s", e)
|
240
|
+
except Exception as e:
|
241
|
+
logging.error("Failed to get marketdata config: %s", e)
|
242
|
+
|
243
|
+
async def set_marketdata(self, venue: Venue, endpoint: str):
|
244
|
+
"""
|
245
|
+
Manually set the marketdata endpoint for a venue.
|
246
|
+
"""
|
247
|
+
try:
|
248
|
+
grpc_host, grpc_port, use_ssl = await resolve_endpoint(endpoint)
|
249
|
+
self.grpc_marketdata[venue] = GrpcClient(
|
250
|
+
host=grpc_host, port=grpc_port, use_ssl=use_ssl
|
251
|
+
)
|
252
|
+
except Exception as e:
|
253
|
+
logging.error("Failed to set marketdata endpoint: %s", e)
|
254
|
+
|
255
|
+
async def marketdata(self, venue: Venue) -> GrpcClient:
|
256
|
+
"""
|
257
|
+
Get the marketdata client for a venue.
|
258
|
+
"""
|
259
|
+
if venue not in self.grpc_marketdata:
|
260
|
+
raise ValueError(f"Marketdata not configured for venue: {venue}")
|
261
|
+
|
262
|
+
await self.refresh_jwt()
|
263
|
+
self.grpc_marketdata[venue].set_jwt(self.jwt)
|
264
|
+
return self.grpc_marketdata[venue]
|
265
|
+
|
266
|
+
async def set_hmart(self, endpoint: str):
|
267
|
+
"""
|
268
|
+
Manually set the hmart (historical marketdata service) endpoint.
|
269
|
+
"""
|
270
|
+
try:
|
271
|
+
grpc_host, grpc_port, use_ssl = await resolve_endpoint(endpoint)
|
272
|
+
logging.info(
|
273
|
+
"Resolved hmart endpoint %s: %s:%d use_ssl=%s",
|
274
|
+
endpoint,
|
275
|
+
grpc_host,
|
276
|
+
grpc_port,
|
277
|
+
use_ssl,
|
278
|
+
)
|
279
|
+
self.grpc_hmart = GrpcClient(
|
280
|
+
host=grpc_host, port=grpc_port, use_ssl=use_ssl
|
281
|
+
)
|
282
|
+
except Exception as e:
|
283
|
+
logging.error("Failed to set hmart endpoint: %s", e)
|
284
|
+
|
285
|
+
async def hmart(self) -> GrpcClient:
|
286
|
+
"""
|
287
|
+
Get the hmart (historical marketdata service) client.
|
288
|
+
"""
|
289
|
+
if self.grpc_hmart is None:
|
290
|
+
# default to historical.marketdata.architect.co
|
291
|
+
await self.set_hmart("https://historical.marketdata.architect.co")
|
292
|
+
|
293
|
+
if self.grpc_hmart is None:
|
294
|
+
raise ValueError("hmart client not initialized")
|
295
|
+
|
296
|
+
await self.refresh_jwt()
|
297
|
+
self.grpc_hmart.set_jwt(self.jwt)
|
298
|
+
return self.grpc_hmart
|
299
|
+
|
300
|
+
async def core(self) -> GrpcClient:
|
301
|
+
"""
|
302
|
+
Get the core client.
|
303
|
+
"""
|
304
|
+
if self.grpc_core is None:
|
305
|
+
raise ValueError("gRPC client to Architect not initialized")
|
306
|
+
|
307
|
+
await self.refresh_jwt()
|
308
|
+
self.grpc_core.set_jwt(self.jwt)
|
309
|
+
return self.grpc_core
|
310
|
+
|
311
|
+
async def who_am_i(self) -> tuple[str, str]:
|
312
|
+
"""
|
313
|
+
Gets the user_id and user_email for the user that the API key belongs to.
|
314
|
+
|
315
|
+
Returns:
|
316
|
+
(user_id, user_email)
|
317
|
+
"""
|
318
|
+
res = await self.graphql_client.user_id_query()
|
319
|
+
return res.user_id, res.user_email
|
209
320
|
|
210
321
|
# ------------------------------------------------------------
|
211
322
|
# Symbology
|
212
323
|
# ------------------------------------------------------------
|
213
324
|
|
325
|
+
async def list_symbols(self, *, marketdata: Optional[Venue] = None) -> list[str]:
|
326
|
+
"""
|
327
|
+
List all symbols.
|
328
|
+
|
329
|
+
Args:
|
330
|
+
marketdata: query marketdata endpoint for the specified venue directly;
|
331
|
+
If provided, query the venue's marketdata endpoint directly,
|
332
|
+
instead of the Architect core. This is sometimes useful for
|
333
|
+
cross-referencing symbols or checking availability.
|
334
|
+
"""
|
335
|
+
if marketdata is not None:
|
336
|
+
grpc_client = await self.marketdata(marketdata)
|
337
|
+
else:
|
338
|
+
grpc_client = await self.core()
|
339
|
+
req = SymbolsRequest()
|
340
|
+
res: SymbolsResponse = await grpc_client.unary_unary(req)
|
341
|
+
return res.symbols
|
342
|
+
|
214
343
|
async def search_symbols(
|
215
344
|
self,
|
216
345
|
search_string: Optional[str] = None,
|
@@ -219,75 +348,70 @@ class AsyncClient:
|
|
219
348
|
limit: int = 20,
|
220
349
|
) -> List[TradableProduct]:
|
221
350
|
"""
|
222
|
-
Search for
|
351
|
+
Search for tradable products on Architect.
|
223
352
|
|
224
353
|
Args:
|
225
|
-
search_string: a string to search for in the symbol
|
354
|
+
search_string: a string to search for in the symbol
|
355
|
+
Can be "*" for wild card search.
|
226
356
|
Examples: "ES", "NQ", "GC"
|
227
357
|
execution_venue: the execution venue to search in
|
228
358
|
Examples: "CME"
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
limit=limit,
|
238
|
-
)
|
239
|
-
).search_symbols
|
240
|
-
|
241
|
-
return markets
|
359
|
+
"""
|
360
|
+
res = await self.graphql_client.search_symbols_query(
|
361
|
+
search_string=search_string,
|
362
|
+
execution_venue=execution_venue,
|
363
|
+
offset=offset,
|
364
|
+
limit=limit,
|
365
|
+
)
|
366
|
+
return res.search_symbols
|
242
367
|
|
243
368
|
async def get_product_info(self, symbol: str) -> Optional[ProductInfoFields]:
|
244
369
|
"""
|
245
|
-
Get
|
370
|
+
Get information about a product, e.g. product_type, underlying, multiplier.
|
246
371
|
|
247
372
|
Args:
|
248
373
|
symbol: the symbol to get information for
|
374
|
+
|
249
375
|
Returns:
|
250
|
-
|
376
|
+
None if the symbol does not exist
|
251
377
|
"""
|
252
|
-
|
253
|
-
return
|
378
|
+
res = await self.graphql_client.get_product_info_query(symbol)
|
379
|
+
return res.product_info
|
254
380
|
|
255
381
|
async def get_product_infos(
|
256
382
|
self, symbols: Optional[list[str]]
|
257
383
|
) -> Sequence[ProductInfoFields]:
|
258
384
|
"""
|
259
|
-
Get
|
385
|
+
Get information about products, e.g. product_type, underlying, multiplier.
|
260
386
|
|
261
387
|
Args:
|
262
|
-
symbols: the symbols to get information for
|
263
|
-
Returns:
|
264
|
-
a list of ProductInfoFields
|
265
|
-
|
266
|
-
Any duplicate or invalid symbols will be ignored.
|
267
|
-
The order of the symbols in the list will not necessarily be preserved in the output.
|
388
|
+
symbols: the symbols to get information for, or None for all symbols
|
268
389
|
|
269
|
-
|
390
|
+
Returns:
|
391
|
+
Product infos for each symbol. Not guaranteed to contain all symbols
|
392
|
+
that were asked for, or in the same order; any duplicates or invalid
|
393
|
+
symbols will be ignored.
|
270
394
|
"""
|
271
|
-
|
272
|
-
return
|
395
|
+
res = await self.graphql_client.get_product_infos_query(symbols)
|
396
|
+
return res.product_infos
|
273
397
|
|
274
398
|
async def get_execution_info(
|
275
|
-
self, symbol: TradableProduct, execution_venue: str
|
399
|
+
self, symbol: TradableProduct | str, execution_venue: str
|
276
400
|
) -> Optional[ExecutionInfoFields]:
|
277
401
|
"""
|
278
|
-
Get
|
402
|
+
Get information about tradable product execution, e.g. tick_size,
|
403
|
+
step_size, margins.
|
279
404
|
|
280
405
|
Args:
|
281
406
|
symbol: the symbol to get execution information for
|
282
407
|
execution_venue: the execution venue e.g. "CME"
|
283
408
|
|
284
409
|
Returns:
|
285
|
-
|
286
|
-
|
410
|
+
None if the symbol doesn't exist
|
287
411
|
"""
|
288
412
|
try:
|
289
413
|
execution_info = await self.graphql_client.get_execution_info_query(
|
290
|
-
symbol, execution_venue
|
414
|
+
TradableProduct(symbol), execution_venue
|
291
415
|
)
|
292
416
|
return execution_info.execution_info
|
293
417
|
except GraphQLClientGraphQLMultiError:
|
@@ -301,59 +425,60 @@ class AsyncClient:
|
|
301
425
|
execution_venue: Optional[str] = None,
|
302
426
|
) -> Sequence[ExecutionInfoFields]:
|
303
427
|
"""
|
304
|
-
Get
|
428
|
+
Get information about tradable product execution, e.g. tick_size,
|
429
|
+
step_size, margins, for many symbols.
|
305
430
|
|
306
431
|
Args:
|
307
|
-
symbols: the symbols to get execution information for
|
432
|
+
symbols: the symbols to get execution information for, or None for all symbols
|
308
433
|
execution_venue: the execution venue e.g. "CME"
|
309
434
|
|
310
435
|
Returns:
|
311
|
-
|
312
|
-
|
313
|
-
|
436
|
+
Execution infos for each symbol. Not guaranteed to contain all symbols
|
437
|
+
that were asked for, or in the same order; any duplicates or invalid
|
438
|
+
symbols will be ignored.
|
314
439
|
"""
|
315
|
-
|
440
|
+
res = await self.graphql_client.get_execution_infos_query(
|
316
441
|
symbols, execution_venue
|
317
442
|
)
|
318
|
-
return
|
443
|
+
return res.execution_infos
|
319
444
|
|
320
445
|
async def get_cme_first_notice_date(self, symbol: str) -> Optional[date]:
|
321
446
|
"""
|
447
|
+
@deprecated(reason="Use get_product_info instead; first_notice_date is now a field")
|
448
|
+
|
322
449
|
Get the first notice date for a CME future.
|
323
450
|
|
324
451
|
Args:
|
325
452
|
symbol: the symbol to get the first notice date for a CME future
|
326
453
|
|
327
454
|
Returns:
|
328
|
-
|
455
|
+
The first notice date as a date object if it exists
|
329
456
|
"""
|
330
|
-
|
331
|
-
if
|
457
|
+
res = await self.graphql_client.get_first_notice_date_query(symbol)
|
458
|
+
if res is None or res.product_info is None:
|
332
459
|
return None
|
333
|
-
return
|
460
|
+
return res.product_info.first_notice_date
|
334
461
|
|
335
|
-
async def
|
462
|
+
async def get_futures_series(self, series_symbol: str) -> list[str]:
|
336
463
|
"""
|
337
|
-
|
464
|
+
List all futures in a given series.
|
338
465
|
|
339
466
|
Args:
|
340
|
-
series_symbol: the
|
341
|
-
e.g. ES CME Futures" would yield a list of all the ES futures
|
467
|
+
series_symbol: the futures series
|
468
|
+
e.g. "ES CME Futures" would yield a list of all the ES futures
|
342
469
|
Returns:
|
343
|
-
|
470
|
+
List of futures products
|
344
471
|
"""
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
futures_series = await self.graphql_client.get_future_series_query(
|
350
|
-
series_symbol
|
351
|
-
)
|
352
|
-
return futures_series.futures_series
|
472
|
+
if not series_symbol.endswith("Futures"):
|
473
|
+
raise ValueError("series_symbol must end with 'Futures'")
|
474
|
+
res = await self.graphql_client.get_future_series_query(series_symbol)
|
475
|
+
return res.futures_series
|
353
476
|
|
354
477
|
@staticmethod
|
355
|
-
def get_expiration_from_CME_name(name: str) -> date:
|
478
|
+
def get_expiration_from_CME_name(name: str) -> Optional[date]:
|
356
479
|
"""
|
480
|
+
@deprecated(reason="Use utils.symbol_parsing.nominative_expiration instead")
|
481
|
+
|
357
482
|
Get the expiration date from a CME future name.
|
358
483
|
|
359
484
|
Args:
|
@@ -362,12 +487,13 @@ class AsyncClient:
|
|
362
487
|
Returns:
|
363
488
|
the expiration date as a date object
|
364
489
|
"""
|
365
|
-
|
366
|
-
return datetime.strptime(d, "%Y%m%d").date()
|
490
|
+
return nominative_expiration(name)
|
367
491
|
|
368
492
|
async def get_cme_futures_series(self, series: str) -> list[tuple[date, str]]:
|
369
493
|
"""
|
370
|
-
|
494
|
+
@deprecated(reason="Use get_futures_series instead")
|
495
|
+
|
496
|
+
List all futures in a given CME series.
|
371
497
|
|
372
498
|
Args:
|
373
499
|
series: the series to get the futures for
|
@@ -383,33 +509,28 @@ class AsyncClient:
|
|
383
509
|
...
|
384
510
|
]
|
385
511
|
"""
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
]
|
393
|
-
|
394
|
-
filtered_markets.sort(key=lambda x: x[0])
|
395
|
-
|
396
|
-
return filtered_markets
|
512
|
+
futures = await self.get_futures_series(series)
|
513
|
+
exp_and_futures = []
|
514
|
+
for future in futures:
|
515
|
+
exp = nominative_expiration(future)
|
516
|
+
if exp is not None:
|
517
|
+
exp_and_futures.append((exp, future))
|
518
|
+
exp_and_futures.sort(key=lambda x: x[0])
|
519
|
+
return exp_and_futures
|
397
520
|
|
398
521
|
async def get_cme_future_from_root_month_year(
|
399
522
|
self, root: str, month: int, year: int
|
400
523
|
) -> str:
|
401
524
|
"""
|
402
525
|
Get the symbol for a CME future from the root, month, and year.
|
526
|
+
This is a simple wrapper around search_symbols.
|
403
527
|
|
404
528
|
Args:
|
405
529
|
root: the root symbol for the future e.g. "ES"
|
406
530
|
month: the month of the future
|
407
531
|
year: the year of the future
|
408
532
|
Returns:
|
409
|
-
|
410
|
-
|
411
|
-
Errors if the result is not unique
|
412
|
-
This is a simple wrapper around search_symbols
|
533
|
+
The future symbol if it exists and is unique.
|
413
534
|
"""
|
414
535
|
[market] = [
|
415
536
|
market
|
@@ -419,23 +540,393 @@ class AsyncClient:
|
|
419
540
|
)
|
420
541
|
if market.startswith(f"{root} {year}{month:02d}")
|
421
542
|
]
|
422
|
-
|
423
543
|
return market
|
424
544
|
|
425
545
|
# ------------------------------------------------------------
|
426
|
-
#
|
546
|
+
# Marketdata
|
427
547
|
# ------------------------------------------------------------
|
428
548
|
|
429
|
-
async def
|
549
|
+
async def get_market_status(
|
550
|
+
self, symbol: TradableProduct | str, venue: Venue
|
551
|
+
) -> MarketStatus:
|
430
552
|
"""
|
431
|
-
|
553
|
+
Returns market status for symbol (e.g. if it's currently quoting or trading).
|
554
|
+
|
555
|
+
Args:
|
556
|
+
symbol: the symbol to get the market status for, e.g. "ES 20250321 CME Future/USD"
|
557
|
+
venue: the venue that the symbol is traded at, e.g. CME
|
558
|
+
"""
|
559
|
+
grpc_client = await self.marketdata(venue)
|
560
|
+
req = MarketStatusRequest(symbol=str(symbol), venue=venue)
|
561
|
+
res: MarketStatus = await grpc_client.unary_unary(req)
|
562
|
+
return res
|
432
563
|
|
564
|
+
async def get_market_snapshot(
|
565
|
+
self, symbol: TradableProduct | str, venue: Venue
|
566
|
+
) -> L1BookSnapshot:
|
567
|
+
"""
|
568
|
+
@deprecated(reason="Use get_l1_snapshot instead")
|
569
|
+
|
570
|
+
This is an alias for l1_book_snapshot.
|
571
|
+
|
572
|
+
Args:
|
573
|
+
symbol: the symbol to get the market snapshot for, e.g. "ES 20250321 CME Future/USD"
|
574
|
+
venue: the venue that the symbol is traded at, e.g. CME
|
433
575
|
Returns:
|
434
|
-
|
576
|
+
MarketTickerFields for the symbol
|
577
|
+
"""
|
578
|
+
return await self.get_l1_book_snapshot(symbol=symbol, venue=venue)
|
579
|
+
|
580
|
+
async def get_market_snapshots(
|
581
|
+
self, symbols: list[TradableProduct | str], venue: Venue
|
582
|
+
) -> Sequence[L1BookSnapshot]:
|
583
|
+
"""
|
584
|
+
@deprecated(reason="Use get_l1_snapshots instead")
|
585
|
+
|
586
|
+
This is an alias for l1_book_snapshots.
|
587
|
+
|
588
|
+
Args:
|
589
|
+
symbols: the symbols to get the market snapshots for
|
590
|
+
venue: the venue that the symbols are traded at
|
591
|
+
"""
|
592
|
+
return await self.get_l1_book_snapshots(
|
593
|
+
venue=venue,
|
594
|
+
symbols=symbols, # type: ignore
|
595
|
+
)
|
596
|
+
|
597
|
+
@overload
|
598
|
+
async def get_historical_candles(
|
599
|
+
self,
|
600
|
+
symbol: TradableProduct | str,
|
601
|
+
venue: Venue,
|
602
|
+
candle_width: CandleWidth,
|
603
|
+
start: datetime,
|
604
|
+
end: datetime,
|
605
|
+
*,
|
606
|
+
as_dataframe: Literal[True],
|
607
|
+
) -> pd.DataFrame: ...
|
608
|
+
|
609
|
+
@overload
|
610
|
+
async def get_historical_candles(
|
611
|
+
self,
|
612
|
+
symbol: TradableProduct | str,
|
613
|
+
venue: Venue,
|
614
|
+
candle_width: CandleWidth,
|
615
|
+
start: datetime,
|
616
|
+
end: datetime,
|
617
|
+
) -> List[Candle]: ...
|
618
|
+
|
619
|
+
async def get_historical_candles(
|
620
|
+
self,
|
621
|
+
symbol: TradableProduct | str,
|
622
|
+
venue: Venue,
|
623
|
+
candle_width: CandleWidth,
|
624
|
+
start: datetime,
|
625
|
+
end: datetime,
|
626
|
+
*,
|
627
|
+
as_dataframe: bool = False,
|
628
|
+
) -> Union[List[Candle], pd.DataFrame]:
|
629
|
+
"""
|
630
|
+
Gets historical candles for a symbol.
|
631
|
+
|
632
|
+
Args:
|
633
|
+
symbol: the symbol to get the candles for
|
634
|
+
venue: the venue of the symbol
|
635
|
+
candle_width: the width of the candles
|
636
|
+
start: the start date to get candles for;
|
637
|
+
For naive datetimes, the server will assume UTC.
|
638
|
+
end: the end date to get candles for;
|
639
|
+
For naive datetimes, the server will assume UTC.
|
640
|
+
as_dataframe: if True, return a pandas DataFrame
|
641
|
+
|
642
|
+
"""
|
643
|
+
grpc_client = await self.hmart()
|
644
|
+
req = HistoricalCandlesRequest(
|
645
|
+
venue=venue,
|
646
|
+
symbol=str(symbol),
|
647
|
+
candle_width=candle_width,
|
648
|
+
start_date=start,
|
649
|
+
end_date=end,
|
650
|
+
)
|
651
|
+
res: HistoricalCandlesResponse = await grpc_client.unary_unary(req)
|
652
|
+
|
653
|
+
if as_dataframe and FEATURE_PANDAS:
|
654
|
+
return candles_to_dataframe(res.candles)
|
655
|
+
elif as_dataframe and not FEATURE_PANDAS:
|
656
|
+
raise RuntimeError("as_dataframe is True but pandas is not installed")
|
657
|
+
else:
|
658
|
+
return res.candles
|
659
|
+
|
660
|
+
async def get_l1_book_snapshot(
|
661
|
+
self,
|
662
|
+
symbol: TradableProduct | str,
|
663
|
+
venue: Venue,
|
664
|
+
) -> L1BookSnapshot:
|
665
|
+
"""
|
666
|
+
Gets the L1 book snapshot for a symbol.
|
667
|
+
|
668
|
+
Args:
|
669
|
+
symbol: the symbol to get the l1 book snapshot for
|
670
|
+
venue: the venue that the symbol is traded at
|
671
|
+
"""
|
672
|
+
grpc_client = await self.marketdata(venue)
|
673
|
+
req = L1BookSnapshotRequest(symbol=str(symbol), venue=venue)
|
674
|
+
res: L1BookSnapshot = await grpc_client.unary_unary(req)
|
675
|
+
return res
|
676
|
+
|
677
|
+
async def get_l1_book_snapshots(
|
678
|
+
self, symbols: list[TradableProduct | str], venue: Venue
|
679
|
+
) -> Sequence[L1BookSnapshot]:
|
680
|
+
"""
|
681
|
+
Gets the L1 book snapshots for a list of symbols.
|
682
|
+
|
683
|
+
Args:
|
684
|
+
symbols: the symbols to get the l1 book snapshots for
|
685
|
+
venue: the venue that the symbols are traded at
|
686
|
+
"""
|
687
|
+
grpc_client = await self.marketdata(venue)
|
688
|
+
req = L1BookSnapshotsRequest(symbols=symbols)
|
689
|
+
res: ArrayOfL1BookSnapshot = await grpc_client.unary_unary(req) # type: ignore
|
690
|
+
return res
|
691
|
+
|
692
|
+
async def get_l2_book_snapshot(
|
693
|
+
self, symbol: TradableProduct | str, venue: Venue
|
694
|
+
) -> L2BookSnapshot:
|
695
|
+
"""
|
696
|
+
Gets the L2 book snapshot for a symbol.
|
697
|
+
|
698
|
+
Args:
|
699
|
+
symbol: the symbol to get the l2 book snapshot for
|
700
|
+
venue: the venue that the symbol is traded at
|
701
|
+
"""
|
702
|
+
grpc_client = await self.marketdata(venue)
|
703
|
+
req = L2BookSnapshotRequest(symbol=str(symbol), venue=venue)
|
704
|
+
res: L2BookSnapshot = await grpc_client.unary_unary(req)
|
705
|
+
return res
|
706
|
+
|
707
|
+
async def get_ticker(self, symbol: TradableProduct | str, venue: Venue) -> Ticker:
|
708
|
+
"""
|
709
|
+
Gets the ticker for a symbol.
|
710
|
+
"""
|
711
|
+
grpc_client = await self.marketdata(venue)
|
712
|
+
req = TickerRequest(symbol=str(symbol), venue=venue)
|
713
|
+
res: Ticker = await grpc_client.unary_unary(req)
|
714
|
+
return res
|
715
|
+
|
716
|
+
async def stream_l1_book_snapshots(
|
717
|
+
self, symbols: Sequence[TradableProduct | str], venue: Venue
|
718
|
+
) -> AsyncIterator[L1BookSnapshot]:
|
719
|
+
"""
|
720
|
+
Subscribe to the stream of L1BookSnapshots for a symbol.
|
721
|
+
|
722
|
+
Args:
|
723
|
+
symbols: the symbols to subscribe to;
|
724
|
+
If symbols=None, subscribe to all symbols available for the venue.
|
725
|
+
venue: the venue to subscribe to
|
726
|
+
"""
|
727
|
+
grpc_client = await self.marketdata(venue)
|
728
|
+
req = SubscribeL1BookSnapshotsRequest(symbols=list(symbols), venue=venue)
|
729
|
+
return grpc_client.unary_stream(req)
|
730
|
+
|
731
|
+
async def stream_l2_book_updates(
|
732
|
+
self, symbol: TradableProduct | str, venue: Venue
|
733
|
+
) -> AsyncIterator[L2BookUpdate]:
|
734
|
+
"""
|
735
|
+
Subscribe to the stream of L2BookUpdates for a symbol.
|
736
|
+
|
737
|
+
This stream is a diff stream; to construct and maintain the actual state of
|
738
|
+
the L2 book, apply the updates stream using the method described.
|
739
|
+
|
740
|
+
Args:
|
741
|
+
symbol: the symbol to subscribe to
|
742
|
+
venue: the marketdata venue
|
743
|
+
"""
|
744
|
+
grpc_client = await self.marketdata(venue)
|
745
|
+
req = SubscribeL2BookUpdatesRequest(symbol=str(symbol), venue=venue)
|
746
|
+
return grpc_client.unary_stream(req) # type: ignore
|
747
|
+
|
748
|
+
async def subscribe_l1_book(
|
749
|
+
self, symbol: TradableProduct | str, venue: Venue
|
750
|
+
) -> L1BookSnapshot:
|
751
|
+
"""
|
752
|
+
Subscribe to the L1 stream for a symbol in the background.
|
753
|
+
|
754
|
+
If a subscription is already active, the existing reference will be
|
755
|
+
returned; otherwise, a new subscription will be created.
|
756
|
+
|
757
|
+
Snapshots will have an initial value of timestamp=0 and bid/ask=None.
|
758
|
+
|
759
|
+
Args:
|
760
|
+
symbol: the symbol to subscribe to
|
761
|
+
venue: the marketdata venue
|
762
|
+
|
763
|
+
Return:
|
764
|
+
An L1 book object that is constantly updating in the background.
|
765
|
+
"""
|
766
|
+
symbol = TradableProduct(symbol)
|
767
|
+
|
768
|
+
if venue in self.l1_books:
|
769
|
+
if symbol in self.l1_books[venue]:
|
770
|
+
return self.l1_books[venue][symbol][0]
|
771
|
+
else:
|
772
|
+
self.l1_books[venue] = {}
|
773
|
+
|
774
|
+
grpc_client = await self.marketdata(venue)
|
775
|
+
book = L1BookSnapshot(symbol, 0, 0)
|
776
|
+
self.l1_books[venue][symbol] = (
|
777
|
+
book,
|
778
|
+
asyncio.create_task(
|
779
|
+
self.__subscribe_l1_book_task(symbol, venue, grpc_client, book)
|
780
|
+
),
|
781
|
+
)
|
782
|
+
return book
|
783
|
+
|
784
|
+
async def unsubscribe_l1_book(self, symbol: TradableProduct | str, venue: Venue):
|
785
|
+
symbol = TradableProduct(symbol)
|
786
|
+
try:
|
787
|
+
task = self.l1_books[venue][symbol][1]
|
788
|
+
task.cancel()
|
789
|
+
except Exception as e:
|
790
|
+
logging.error(
|
791
|
+
f"Error unsubscribing from L1 book for {symbol}, venue {venue}: {e}"
|
792
|
+
)
|
793
|
+
finally:
|
794
|
+
if venue in self.l1_books and symbol in self.l1_books[venue]:
|
795
|
+
del self.l1_books[venue][symbol]
|
796
|
+
|
797
|
+
async def __subscribe_l1_book_task(
|
798
|
+
self,
|
799
|
+
symbol: TradableProduct,
|
800
|
+
venue: Venue,
|
801
|
+
grpc_client: GrpcClient,
|
802
|
+
book: L1BookSnapshot,
|
803
|
+
):
|
804
|
+
try:
|
805
|
+
req = SubscribeL1BookSnapshotsRequest(symbols=[symbol], venue=venue)
|
806
|
+
stream = grpc_client.unary_stream(req)
|
807
|
+
async for snap in stream:
|
808
|
+
book.tn = snap.tn
|
809
|
+
book.ts = snap.ts
|
810
|
+
book.a = snap.a
|
811
|
+
book.b = snap.b
|
812
|
+
book.rt = snap.rt
|
813
|
+
book.rtn = snap.rtn
|
814
|
+
except Exception as e:
|
815
|
+
logging.error(
|
816
|
+
f"Error subscribing to L1 book for {symbol}, venue {venue}: {e}"
|
817
|
+
)
|
818
|
+
finally:
|
819
|
+
del self.l1_books[venue][symbol]
|
820
|
+
|
821
|
+
async def subscribe_l2_book(
|
822
|
+
self,
|
823
|
+
symbol: TradableProduct | str,
|
824
|
+
venue: Venue,
|
825
|
+
) -> L2BookSnapshot:
|
826
|
+
"""
|
827
|
+
Subscribe to the L2 stream for a symbol in the background.
|
828
|
+
|
829
|
+
If a subscription is already active, the existing reference will be
|
830
|
+
returned; otherwise, a new subscription will be created.
|
831
|
+
|
832
|
+
Snapshots will have an initial value of timestamp=0 and bids/asks=[].
|
833
|
+
|
834
|
+
Args:
|
835
|
+
symbol: the symbol to subscribe to
|
836
|
+
venue: the marketdata venue
|
837
|
+
|
838
|
+
Return:
|
839
|
+
An L2 book object that is constantly updating in the background.
|
840
|
+
"""
|
841
|
+
symbol = TradableProduct(symbol)
|
842
|
+
|
843
|
+
if venue in self.l2_books:
|
844
|
+
if symbol in self.l2_books[venue]:
|
845
|
+
return self.l2_books[venue][symbol][0]
|
846
|
+
else:
|
847
|
+
self.l2_books[venue] = {}
|
848
|
+
|
849
|
+
grpc_client = await self.marketdata(venue)
|
850
|
+
book = L2BookSnapshot([], [], 0, 0, 0, 0)
|
851
|
+
self.l2_books[venue][symbol] = (
|
852
|
+
book,
|
853
|
+
asyncio.create_task(
|
854
|
+
self.__subscribe_l2_book_task(symbol, venue, grpc_client, book)
|
855
|
+
),
|
856
|
+
)
|
857
|
+
return book
|
858
|
+
|
859
|
+
async def __subscribe_l2_book_task(
|
860
|
+
self,
|
861
|
+
symbol: TradableProduct,
|
862
|
+
venue: Venue,
|
863
|
+
grpc_client: GrpcClient,
|
864
|
+
book: L2BookSnapshot,
|
865
|
+
):
|
866
|
+
try:
|
867
|
+
req = SubscribeL2BookUpdatesRequest(symbol=str(symbol), venue=venue)
|
868
|
+
stream = grpc_client.unary_stream(req) # type: ignore
|
869
|
+
async for up in stream:
|
870
|
+
if isinstance(up, L2BookDiff):
|
871
|
+
if (
|
872
|
+
up.sequence_id != book.sequence_id
|
873
|
+
or up.sequence_number != book.sequence_number + 1
|
874
|
+
):
|
875
|
+
raise ValueError(
|
876
|
+
f"Received update out-of-order for L2 book: {symbol}"
|
877
|
+
)
|
878
|
+
book.sid = up.sid
|
879
|
+
book.sn = up.sn
|
880
|
+
book.ts = up.ts
|
881
|
+
book.tn = up.tn
|
882
|
+
for px, sz in up.bids:
|
883
|
+
update_orderbook_side(book.bids, px, sz, ascending=False)
|
884
|
+
for px, sz in up.asks:
|
885
|
+
update_orderbook_side(book.asks, px, sz, ascending=True)
|
886
|
+
elif isinstance(up, L2BookSnapshot):
|
887
|
+
book.sid = up.sid
|
888
|
+
book.sn = up.sn
|
889
|
+
book.ts = up.ts
|
890
|
+
book.tn = up.tn
|
891
|
+
book.a = up.a
|
892
|
+
book.b = up.b
|
893
|
+
except Exception as e:
|
894
|
+
logging.error(
|
895
|
+
f"Error subscribing to L2 book for {symbol}, venue {venue}: {e}"
|
896
|
+
)
|
897
|
+
finally:
|
898
|
+
del self.l2_books[venue][symbol]
|
899
|
+
|
900
|
+
async def stream_trades(
|
901
|
+
self, symbol: TradableProduct | str, venue: Venue
|
902
|
+
) -> AsyncIterator[Trade]:
|
903
|
+
"""
|
904
|
+
Subscribe to a stream of trades for a symbol.
|
905
|
+
"""
|
906
|
+
grpc_client = await self.marketdata(venue)
|
907
|
+
req = SubscribeTradesRequest(symbol=str(symbol), venue=venue)
|
908
|
+
return grpc_client.unary_stream(req)
|
909
|
+
|
910
|
+
async def stream_candles(
|
911
|
+
self,
|
912
|
+
symbol: TradableProduct | str,
|
913
|
+
venue: Venue,
|
914
|
+
candle_widths: Optional[list[CandleWidth]],
|
915
|
+
) -> AsyncIterator[Candle]:
|
916
|
+
"""
|
917
|
+
Subscribe to a stream of candles for a symbol.
|
435
918
|
"""
|
436
|
-
|
919
|
+
grpc_client = await self.marketdata(venue)
|
920
|
+
req = SubscribeCandlesRequest(
|
921
|
+
symbol=str(symbol),
|
922
|
+
venue=venue,
|
923
|
+
candle_widths=candle_widths,
|
924
|
+
)
|
925
|
+
return grpc_client.unary_stream(req)
|
437
926
|
|
438
|
-
|
927
|
+
# ------------------------------------------------------------
|
928
|
+
# Portfolio management
|
929
|
+
# ------------------------------------------------------------
|
439
930
|
|
440
931
|
async def list_accounts(self) -> Sequence[AccountWithPermissionsFields]:
|
441
932
|
"""
|
@@ -443,23 +934,35 @@ class AsyncClient:
|
|
443
934
|
|
444
935
|
Returns:
|
445
936
|
a list of AccountWithPermissionsFields for the user that the API key belongs to
|
937
|
+
a list of AccountWithPermissions for the user that the API key belongs to
|
446
938
|
(use who_am_i to get the user_id / email)
|
447
939
|
"""
|
448
|
-
|
940
|
+
res = await self.graphql_client.list_accounts_query()
|
941
|
+
return res.accounts
|
942
|
+
|
943
|
+
"""
|
944
|
+
async def list_accounts(self) -> List[grpc_definitions.AccountWithPermissions]:
|
945
|
+
request = AccountsRequest()
|
946
|
+
accounts = await self.grpc_client.request(request)
|
449
947
|
return accounts.accounts
|
948
|
+
"""
|
450
949
|
|
451
950
|
async def get_account_summary(self, account: str) -> AccountSummaryFields:
|
452
951
|
"""
|
453
|
-
|
952
|
+
Get account summary, including balances, positions, pnls, etc.
|
454
953
|
|
455
954
|
Args:
|
456
|
-
account:
|
457
|
-
|
458
|
-
|
459
|
-
|
955
|
+
account: account uuid or name
|
956
|
+
Examples: "00000000-0000-0000-0000-000000000000", "STONEX:000000/JDoe"
|
957
|
+
"""
|
958
|
+
res = await self.graphql_client.get_account_summary_query(account=account)
|
959
|
+
return res.account_summary
|
960
|
+
"""
|
961
|
+
async def get_account_summary(self, account: str) -> AccountSummary:
|
962
|
+
request = AccountSummaryRequest(account=account)
|
963
|
+
account_summary = await self.grpc_client.request(request)
|
964
|
+
return account_summary
|
460
965
|
"""
|
461
|
-
summary = await self.graphql_client.get_account_summary_query(account=account)
|
462
|
-
return summary.account_summary
|
463
966
|
|
464
967
|
async def get_account_summaries(
|
465
968
|
self,
|
@@ -467,21 +970,31 @@ class AsyncClient:
|
|
467
970
|
trader: Optional[str] = None,
|
468
971
|
) -> Sequence[AccountSummaryFields]:
|
469
972
|
"""
|
470
|
-
|
973
|
+
Get account summaries for accounts matching the filters.
|
471
974
|
|
472
975
|
Args:
|
473
|
-
accounts:
|
474
|
-
|
475
|
-
trader: the trader / userId to get summaries for
|
976
|
+
accounts: list of account uuids or names
|
977
|
+
trader: if specified, return summaries for all accounts for this trader
|
476
978
|
|
477
|
-
|
478
|
-
Returns:
|
479
|
-
a list of AccountSummary for the accounts
|
979
|
+
If both arguments are given, the union of matching accounts are returned.
|
480
980
|
"""
|
481
|
-
|
981
|
+
res = await self.graphql_client.get_account_summaries_query(
|
482
982
|
trader=trader, accounts=accounts
|
483
983
|
)
|
484
|
-
return
|
984
|
+
return res.account_summaries
|
985
|
+
"""
|
986
|
+
async def get_account_summaries(
|
987
|
+
self,
|
988
|
+
accounts: Optional[list[str]] = None,
|
989
|
+
trader: Optional[str] = None,
|
990
|
+
) -> list[AccountSummary]:
|
991
|
+
request = AccountSummariesRequest(
|
992
|
+
accounts=accounts,
|
993
|
+
trader=trader,
|
994
|
+
)
|
995
|
+
account_summaries = await self.grpc_client.request(request)
|
996
|
+
return account_summaries.account_summaries
|
997
|
+
"""
|
485
998
|
|
486
999
|
async def get_account_history(
|
487
1000
|
self,
|
@@ -490,19 +1003,43 @@ class AsyncClient:
|
|
490
1003
|
to_exclusive: Optional[datetime] = None,
|
491
1004
|
) -> Sequence[AccountSummaryFields]:
|
492
1005
|
"""
|
493
|
-
|
1006
|
+
Get historical sequence of account summaries for the given account.
|
1007
|
+
"""
|
1008
|
+
res = await self.graphql_client.get_account_history_query(
|
1009
|
+
account=account, from_inclusive=from_inclusive, to_exclusive=to_exclusive
|
1010
|
+
)
|
1011
|
+
return res.account_history
|
494
1012
|
|
495
|
-
Returns:
|
496
|
-
a list of AccountSummaryFields for the account for the given dates
|
497
|
-
use timestamp to get the time of the of the summary
|
498
1013
|
"""
|
499
|
-
|
1014
|
+
async def get_account_history(
|
1015
|
+
self,
|
1016
|
+
account: str,
|
1017
|
+
from_inclusive: Optional[datetime] = None,
|
1018
|
+
to_exclusive: Optional[datetime] = None,
|
1019
|
+
) -> list[AccountSummary]:
|
1020
|
+
if from_inclusive is not None:
|
1021
|
+
assert from_inclusive.tzinfo is timezone.utc, (
|
1022
|
+
"from_inclusive must be a utc datetime:\n"
|
1023
|
+
"for example datetime.now(timezone.utc) or \n"
|
1024
|
+
"dt = datetime(2025, 4, 15, 12, 0, 0, tzinfo=timezone.utc)"
|
1025
|
+
)
|
1026
|
+
|
1027
|
+
if to_exclusive is not None:
|
1028
|
+
assert to_exclusive.tzinfo is timezone.utc, (
|
1029
|
+
"to_exclusive must be a utc datetime:\n"
|
1030
|
+
"for example datetime.now(timezone.utc) or \n"
|
1031
|
+
"dt = datetime(2025, 4, 15, 12, 0, 0, tzinfo=timezone.utc)"
|
1032
|
+
)
|
1033
|
+
|
1034
|
+
request = AccountHistoryRequest(
|
500
1035
|
account=account, from_inclusive=from_inclusive, to_exclusive=to_exclusive
|
501
1036
|
)
|
502
|
-
|
1037
|
+
history = await self.grpc_client.request(request)
|
1038
|
+
return history.history
|
1039
|
+
"""
|
503
1040
|
|
504
1041
|
# ------------------------------------------------------------
|
505
|
-
# Order
|
1042
|
+
# Order management
|
506
1043
|
# ------------------------------------------------------------
|
507
1044
|
|
508
1045
|
async def get_open_orders(
|
@@ -525,12 +1062,10 @@ class AsyncClient:
|
|
525
1062
|
symbol: the symbol to get orders for
|
526
1063
|
parent_order_id: the parent order id to get orders for
|
527
1064
|
|
528
|
-
these filters are combinewd via OR statements so if you pass
|
529
|
-
in multiple arguments, it will return the union of the results
|
530
1065
|
Returns:
|
531
|
-
|
1066
|
+
Open orders that match the union of the filters
|
532
1067
|
"""
|
533
|
-
|
1068
|
+
res = await self.graphql_client.get_open_orders_query(
|
534
1069
|
venue=venue,
|
535
1070
|
account=account,
|
536
1071
|
trader=trader,
|
@@ -538,17 +1073,16 @@ class AsyncClient:
|
|
538
1073
|
parent_order_id=parent_order_id,
|
539
1074
|
order_ids=order_ids,
|
540
1075
|
)
|
541
|
-
return
|
1076
|
+
return res.open_orders
|
542
1077
|
|
543
1078
|
async def get_all_open_orders(self) -> Sequence[OrderFields]:
|
544
1079
|
"""
|
545
|
-
|
1080
|
+
@deprecated(reason="Use get_open_orders with no parameters instead")
|
546
1081
|
|
547
|
-
Returns
|
548
|
-
a list of OrderFields of all the open orders for the user
|
1082
|
+
Returns a list of all open orders for the authenticated user.
|
549
1083
|
"""
|
550
|
-
|
551
|
-
return
|
1084
|
+
res = await self.graphql_client.get_open_orders_query()
|
1085
|
+
return res.open_orders
|
552
1086
|
|
553
1087
|
async def get_historical_orders(
|
554
1088
|
self,
|
@@ -560,24 +1094,25 @@ class AsyncClient:
|
|
560
1094
|
parent_order_id: Optional[str] = None,
|
561
1095
|
) -> Sequence[OrderFields]:
|
562
1096
|
"""
|
563
|
-
|
1097
|
+
Returns a list of all historical orders that match the filters.
|
1098
|
+
|
1099
|
+
Historical orders are orders that are not open, having been filled,
|
1100
|
+
canceled, expired, or outed.
|
564
1101
|
|
565
1102
|
Args:
|
566
1103
|
order_ids: a list of order ids to get
|
567
1104
|
from_inclusive: the start date to get orders for
|
568
1105
|
to_exclusive: the end date to get orders for
|
569
1106
|
venue: the venue to get orders for, e.g. CME
|
570
|
-
account:
|
571
|
-
can be the account id( (a UUID) or the account name (e.g. CQG:00000)
|
1107
|
+
account: account uuid or name
|
572
1108
|
parent_order_id: the parent order id to get orders for
|
573
1109
|
Returns:
|
574
|
-
|
1110
|
+
Historical orders that match the union of the filters.
|
575
1111
|
|
576
|
-
|
577
|
-
|
578
|
-
the from_inclusive and to_exclusive parameters need to be filled
|
1112
|
+
If order_ids is not specified, then from_inclusive and to_exclusive
|
1113
|
+
MUST be specified.
|
579
1114
|
"""
|
580
|
-
|
1115
|
+
res = await self.graphql_client.get_historical_orders_query(
|
581
1116
|
order_ids=order_ids,
|
582
1117
|
venue=venue,
|
583
1118
|
account=account,
|
@@ -585,54 +1120,41 @@ class AsyncClient:
|
|
585
1120
|
from_inclusive=from_inclusive,
|
586
1121
|
to_exclusive=to_exclusive,
|
587
1122
|
)
|
588
|
-
return
|
1123
|
+
return res.historical_orders
|
589
1124
|
|
590
1125
|
async def get_order(self, order_id: str) -> Optional[OrderFields]:
|
591
1126
|
"""
|
592
|
-
Returns the
|
593
|
-
|
1127
|
+
Returns the specified order. Useful for looking at past sent orders.
|
1128
|
+
Queries open_orders first, then queries historical_orders.
|
594
1129
|
|
595
1130
|
Args:
|
596
1131
|
order_id: the order id to get
|
597
|
-
Returns:
|
598
|
-
the OrderFields object for the order
|
599
|
-
|
600
|
-
Queries open_orders first then queries historical_orders
|
601
1132
|
"""
|
602
|
-
|
603
|
-
|
604
|
-
)
|
605
|
-
|
606
|
-
for open_order in open_orders.open_orders:
|
1133
|
+
res = await self.graphql_client.get_open_orders_query(order_ids=[order_id])
|
1134
|
+
for open_order in res.open_orders:
|
607
1135
|
if open_order.id == order_id:
|
608
1136
|
return open_order
|
609
1137
|
|
610
|
-
|
1138
|
+
res = await self.graphql_client.get_historical_orders_query(
|
611
1139
|
order_ids=[order_id]
|
612
1140
|
)
|
613
|
-
|
614
|
-
|
615
|
-
return historical_orders.historical_orders[0]
|
1141
|
+
if res.historical_orders and len(res.historical_orders) > 0:
|
1142
|
+
return res.historical_orders[0]
|
616
1143
|
|
617
1144
|
async def get_orders(self, order_ids: list[str]) -> list[Optional[OrderFields]]:
|
618
1145
|
"""
|
619
|
-
Returns
|
620
|
-
|
1146
|
+
Returns the specified orders. Useful for looking at past sent orders.
|
1147
|
+
Plural form of get_order.
|
621
1148
|
|
622
1149
|
Args:
|
623
1150
|
order_ids: a list of order ids to get
|
624
|
-
Returns:
|
625
|
-
a list of OrderFields objects for the orders
|
626
|
-
|
627
|
-
Plural form of get_order
|
628
1151
|
"""
|
629
1152
|
orders_dict: dict[str, Optional[OrderFields]] = {
|
630
1153
|
order_id: None for order_id in order_ids
|
631
1154
|
}
|
632
1155
|
|
633
|
-
|
634
|
-
|
635
|
-
).open_orders
|
1156
|
+
res = await self.graphql_client.get_open_orders_query(order_ids=order_ids)
|
1157
|
+
open_orders = res.open_orders
|
636
1158
|
for open_order in open_orders:
|
637
1159
|
orders_dict[open_order.id] = open_order
|
638
1160
|
|
@@ -640,11 +1162,10 @@ class AsyncClient:
|
|
640
1162
|
order_id for order_id in order_ids if orders_dict[order_id] is None
|
641
1163
|
]
|
642
1164
|
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
).historical_orders
|
1165
|
+
res = await self.graphql_client.get_historical_orders_query(
|
1166
|
+
order_ids=not_open_order_ids
|
1167
|
+
)
|
1168
|
+
historical_orders = res.historical_orders
|
648
1169
|
for historical_order in historical_orders:
|
649
1170
|
orders_dict[historical_order.id] = historical_order
|
650
1171
|
|
@@ -652,311 +1173,96 @@ class AsyncClient:
|
|
652
1173
|
|
653
1174
|
async def get_fills(
|
654
1175
|
self,
|
655
|
-
from_inclusive: Optional[datetime],
|
656
|
-
to_exclusive: Optional[datetime],
|
1176
|
+
from_inclusive: Optional[datetime] = None,
|
1177
|
+
to_exclusive: Optional[datetime] = None,
|
657
1178
|
venue: Optional[str] = None,
|
658
1179
|
account: Optional[str] = None,
|
659
1180
|
order_id: Optional[str] = None,
|
660
1181
|
) -> GetFillsQueryFolioHistoricalFills:
|
661
1182
|
"""
|
662
|
-
Returns
|
1183
|
+
Returns all fills matching the given filters.
|
663
1184
|
|
664
1185
|
Args:
|
665
1186
|
from_inclusive: the start date to get fills for
|
666
1187
|
to_exclusive: the end date to get fills for
|
667
1188
|
venue: the venue to get fills for, e.g. "CME"
|
668
|
-
account:
|
669
|
-
can be the account id( (a UUID) or the account name (e.g. CQG:00000)
|
1189
|
+
account: account uuid or name
|
670
1190
|
order_id: the order id to get fills for
|
671
|
-
Returns:
|
672
|
-
a list of GetFillsQueryFolioHistoricalFills
|
673
1191
|
"""
|
674
|
-
|
1192
|
+
res = await self.graphql_client.get_fills_query(
|
675
1193
|
venue, account, order_id, from_inclusive, to_exclusive
|
676
1194
|
)
|
677
|
-
return
|
678
|
-
|
679
|
-
# ------------------------------------------------------------
|
680
|
-
# Market Data
|
681
|
-
# ------------------------------------------------------------
|
682
|
-
|
683
|
-
async def get_market_status(
|
684
|
-
self, symbol: TradableProduct, venue: str
|
685
|
-
) -> MarketStatusFields:
|
686
|
-
"""
|
687
|
-
Returns market status for symbol (ie if it is quoting and trading).
|
1195
|
+
return res.historical_fills
|
688
1196
|
|
689
|
-
|
690
|
-
symbol: the symbol to get the market status for, e.g. "ES 20250321 CME Future/USD"
|
691
|
-
venue: the venue that the symbol is traded at, e.g. CME
|
692
|
-
Returns:
|
693
|
-
MarketStatusFields for the symbol
|
694
|
-
"""
|
695
|
-
market_status = await self.graphql_client.get_market_status_query(symbol, venue)
|
696
|
-
return market_status.market_status
|
697
|
-
|
698
|
-
async def get_market_snapshot(
|
699
|
-
self, symbol: TradableProduct, venue: str
|
700
|
-
) -> MarketTickerFields:
|
701
|
-
"""
|
702
|
-
This is an alias for l1_book_snapshot.
|
703
|
-
|
704
|
-
Args:
|
705
|
-
symbol: the symbol to get the market snapshot for, e.g. "ES 20250321 CME Future/USD"
|
706
|
-
venue: the venue that the symbol is traded at, e.g. CME
|
707
|
-
Returns:
|
708
|
-
MarketTickerFields for the symbol
|
709
|
-
"""
|
710
|
-
return await self.get_l1_book_snapshot(symbol=symbol, venue=venue)
|
711
|
-
|
712
|
-
async def get_market_snapshots(
|
713
|
-
self, symbols: list[TradableProduct], venue: str
|
714
|
-
) -> Sequence[MarketTickerFields]:
|
715
|
-
"""
|
716
|
-
This is an alias for l1_book_snapshot.
|
717
|
-
|
718
|
-
Args:
|
719
|
-
symbols: the symbols to get the market snapshots for
|
720
|
-
venue: the venue that the symbols are traded at
|
721
|
-
Returns:
|
722
|
-
a list of MarketTickerFields for the symbols
|
723
|
-
"""
|
724
|
-
return await self.get_l1_book_snapshots(
|
725
|
-
venue=venue, symbols=symbols # type: ignore
|
726
|
-
)
|
727
|
-
|
728
|
-
async def get_historical_candles(
|
1197
|
+
async def orderflow(
|
729
1198
|
self,
|
730
|
-
|
731
|
-
|
732
|
-
start: datetime,
|
733
|
-
end: datetime,
|
734
|
-
) -> HistoricalCandlesResponse:
|
1199
|
+
request_iterator: AsyncIterator[OrderflowRequest],
|
1200
|
+
) -> AsyncIterator[Orderflow]:
|
735
1201
|
"""
|
736
|
-
|
1202
|
+
A two-way channel for both order entry and listening to order updates (fills, acks, outs, etc.).
|
737
1203
|
|
738
|
-
|
739
|
-
symbol: the symbol to get the candles for
|
740
|
-
venue: the venue of the symbol
|
741
|
-
candle_width: the width of the candles
|
742
|
-
start: the start date to get candles for
|
743
|
-
end: the end date to get candles for
|
744
|
-
Returns:
|
745
|
-
a list of CandleFields for the specified candles
|
746
|
-
"""
|
747
|
-
request = HistoricalCandlesRequest(
|
748
|
-
symbol=symbol,
|
749
|
-
candle_width=candle_width,
|
750
|
-
start_date=start,
|
751
|
-
end_date=end,
|
752
|
-
)
|
1204
|
+
This is considered the most efficient way to trade in this SDK.
|
753
1205
|
|
754
|
-
|
1206
|
+
Example:
|
1207
|
+
See test_orderflow.py for an example.
|
755
1208
|
|
756
|
-
|
757
|
-
self,
|
758
|
-
symbol: str,
|
759
|
-
venue: str,
|
760
|
-
) -> MarketTickerFields:
|
1209
|
+
This WILL block the event loop until the stream is closed.
|
761
1210
|
"""
|
762
|
-
|
763
|
-
|
764
|
-
|
765
|
-
|
766
|
-
|
767
|
-
|
768
|
-
MarketTickerFields for the symbol
|
769
|
-
"""
|
770
|
-
snapshot = await self.graphql_client.get_l_1_book_snapshot_query(
|
771
|
-
symbol=symbol, venue=venue
|
1211
|
+
grpc_client = await self.core()
|
1212
|
+
decoder = grpc_client.get_decoder(OrderflowRequestUnannotatedResponseType)
|
1213
|
+
stub = grpc_client.channel.stream_stream(
|
1214
|
+
OrderflowRequest_route,
|
1215
|
+
request_serializer=grpc_client.encoder().encode,
|
1216
|
+
response_deserializer=decoder.decode,
|
772
1217
|
)
|
773
|
-
|
774
|
-
|
775
|
-
async def get_l1_book_snapshots(
|
776
|
-
self, symbols: list[str], venue: str
|
777
|
-
) -> Sequence[MarketTickerFields]:
|
778
|
-
"""
|
779
|
-
Gets the L1 book snapshots for a list of symbols.
|
780
|
-
|
781
|
-
Args:
|
782
|
-
symbols: the symbols to get the l1 book snapshots for
|
783
|
-
venue: the venue that the symbols are traded at
|
784
|
-
Returns:
|
785
|
-
a list of MarketTickerFields for the symbols
|
786
|
-
"""
|
787
|
-
snapshot = await self.graphql_client.get_l_1_book_snapshots_query(
|
788
|
-
venue=venue, symbols=symbols
|
789
|
-
)
|
790
|
-
return snapshot.tickers
|
791
|
-
|
792
|
-
async def get_l2_book_snapshot(self, symbol: str, venue: str) -> L2BookFields:
|
793
|
-
"""
|
794
|
-
Gets the L2 book snapshot for a symbol.
|
795
|
-
|
796
|
-
Args:
|
797
|
-
symbol: the symbol to get the l2 book snapshot for
|
798
|
-
venue: the venue that the symbol is traded at
|
799
|
-
Returns:
|
800
|
-
L2BookFields for the symbol
|
801
|
-
|
802
|
-
Note: this does NOT update, it is a snapshot at a given time
|
803
|
-
For an object that updates, use subscribe_l2_book
|
804
|
-
"""
|
805
|
-
l2_book = await self.graphql_client.get_l_2_book_snapshot_query(
|
806
|
-
symbol=symbol, venue=venue
|
1218
|
+
call = stub(
|
1219
|
+
request_iterator, metadata=(("authorization", f"Bearer {grpc_client.jwt}"),)
|
807
1220
|
)
|
808
|
-
|
809
|
-
|
810
|
-
async def subscribe_l1_book_stream(
|
811
|
-
self, symbols: list[TradableProduct], venue: str
|
812
|
-
) -> AsyncIterator[L1BookSnapshot]:
|
813
|
-
"""
|
814
|
-
Subscribe to the stream of L1BookSnapshots for a symbol.
|
815
|
-
|
816
|
-
Args:
|
817
|
-
symbol: the symbol to subscribe to
|
818
|
-
venue: the venue to subscribe to
|
819
|
-
Returns:
|
820
|
-
an async iterator that yields L1BookSnapshot, representing the l1 book updates
|
821
|
-
"""
|
822
|
-
async for snapshot in await self.grpc_client.subscribe_l1_books_stream(
|
823
|
-
symbols=[str(s) for s in symbols]
|
824
|
-
):
|
825
|
-
yield snapshot
|
826
|
-
|
827
|
-
async def subscribe_l2_book_stream(
|
828
|
-
self, symbol: TradableProduct, venue: str
|
829
|
-
) -> AsyncIterator[L2BookUpdate]:
|
830
|
-
"""
|
831
|
-
Subscribe to the stream of L2BookUpdates for a symbol.
|
832
|
-
|
833
|
-
IMPORTANT: note that the Snapshot is a different type than
|
834
|
-
L2BookSnapshot
|
835
|
-
Args:
|
836
|
-
symbol: the symbol to subscribe to
|
837
|
-
venue: the venue to subscribe to
|
838
|
-
Returns:
|
839
|
-
an async iterator that yields L2BookFields
|
840
|
-
L2BookFields is either a Snapshot or a Diff
|
841
|
-
See the grpc_client code for how to handle the different types
|
842
|
-
"""
|
843
|
-
async for snapshot in self.grpc_client.subscribe_l2_books_stream(
|
844
|
-
symbol=symbol, venue=venue
|
845
|
-
):
|
846
|
-
yield snapshot
|
1221
|
+
async for update in call:
|
1222
|
+
yield update
|
847
1223
|
|
848
|
-
async def
|
849
|
-
self, symbols: list[TradableProduct]
|
850
|
-
) -> list[L1BookSnapshot]:
|
851
|
-
"""
|
852
|
-
Returns a L1BookSnapshot object that is constantly updating in the background.
|
853
|
-
|
854
|
-
Args:
|
855
|
-
symbols: the symbols to subscribe to
|
856
|
-
Return:
|
857
|
-
a list of L1BookSnapshot objects that are constantly updating in the background
|
858
|
-
For the duration of the program, the client will be subscribed to the stream
|
859
|
-
and be updating the L1BookSnapshot.
|
860
|
-
|
861
|
-
IMPORTANT: The L1BookSnapshot will be initialized with
|
862
|
-
a timestamp (field tn and ts) of 0
|
863
|
-
along with None for bid and ask
|
864
|
-
|
865
|
-
The reference to the object should be kept, but can also be referenced via
|
866
|
-
client.grpc_client.l1_books.get(symbol)
|
867
|
-
|
868
|
-
If you want direct access to the stream to do on_update type code, you can
|
869
|
-
call client.grpc_client.stream_l1_books
|
870
|
-
"""
|
871
|
-
books = self.grpc_client.initialize_l1_books(symbols)
|
872
|
-
asyncio.create_task(self.grpc_client.watch_l1_books(symbols=symbols))
|
873
|
-
i = 0
|
874
|
-
while not all(book.ts > 0 for book in books) and i < 10:
|
875
|
-
await asyncio.sleep(0.2)
|
876
|
-
i += 1
|
877
|
-
if i == 10:
|
878
|
-
raise ValueError(
|
879
|
-
f"Could not get L1 books for {symbols}. Check if market is quoting via client.get_market_status."
|
880
|
-
)
|
881
|
-
|
882
|
-
return books
|
883
|
-
|
884
|
-
async def subscribe_l2_book(
|
1224
|
+
async def stream_orderflow(
|
885
1225
|
self,
|
886
|
-
|
887
|
-
|
888
|
-
|
1226
|
+
account: Optional[grpc_definitions.AccountIdOrName] = None,
|
1227
|
+
execution_venue: Optional[str] = None,
|
1228
|
+
trader: Optional[grpc_definitions.TraderIdOrEmail] = None,
|
1229
|
+
) -> AsyncIterator[Orderflow]:
|
889
1230
|
"""
|
890
|
-
|
1231
|
+
A stream for listening to order updates (fills, acks, outs, etc.).
|
891
1232
|
|
892
|
-
|
893
|
-
|
894
|
-
|
895
|
-
|
896
|
-
|
897
|
-
|
898
|
-
|
899
|
-
IMPORTANT: The LBBookSnapshot will be initialized with
|
900
|
-
a timestamp (field tn and ts) of 0
|
901
|
-
along with None for bid and ask
|
902
|
-
|
903
|
-
The reference to the object should be kept, but can also be referenced via
|
904
|
-
client.grpc_client.l1_books.get(symbol)
|
905
|
-
|
906
|
-
If you want direct access to the stream to do on_update type code, you can
|
907
|
-
call client.grpc_client.stream_l2_book
|
908
|
-
"""
|
909
|
-
book = self.grpc_client.initialize_l2_book(symbol, venue)
|
910
|
-
asyncio.create_task(self.grpc_client.watch_l2_book(symbol, venue))
|
911
|
-
i = 0
|
912
|
-
while book.ts == 0 and i < 10:
|
913
|
-
await asyncio.sleep(0.2)
|
914
|
-
i += 1
|
915
|
-
if book.ts == 0:
|
916
|
-
if venue:
|
917
|
-
market_status = await self.get_market_status(symbol, venue)
|
918
|
-
if not market_status.is_quoting:
|
919
|
-
raise ValueError(
|
920
|
-
f"Market {symbol} is currently closed, cannot get L2."
|
921
|
-
)
|
922
|
-
raise ValueError(f"Could not get L2 book for {symbol}.")
|
923
|
-
|
924
|
-
return book
|
1233
|
+
Example:
|
1234
|
+
```python
|
1235
|
+
request = SubscribeOrderflowRequest.new()
|
1236
|
+
async for of in client.subscribe_orderflow_stream(request):
|
1237
|
+
print(of)
|
1238
|
+
```
|
925
1239
|
|
926
|
-
|
927
|
-
self, symbol: TradableProduct, venue: Optional[str]
|
928
|
-
) -> AsyncIterator[Trade]:
|
929
|
-
"""
|
930
|
-
Subscribe to a stream of trades for a symbol.
|
1240
|
+
This WILL block the event loop until the stream is closed.
|
931
1241
|
"""
|
932
|
-
|
933
|
-
|
934
|
-
|
935
|
-
|
936
|
-
self
|
937
|
-
|
938
|
-
|
939
|
-
|
940
|
-
|
941
|
-
|
942
|
-
Subscribe to a stream of candles for a symbol.
|
943
|
-
"""
|
944
|
-
request = SubscribeCandlesRequest(
|
945
|
-
symbol=str(symbol),
|
946
|
-
venue=venue,
|
947
|
-
candle_widths=candle_widths,
|
1242
|
+
grpc_client = await self.core()
|
1243
|
+
request: SubscribeOrderflowRequest = SubscribeOrderflowRequest(
|
1244
|
+
account=account, execution_venue=execution_venue, trader=trader
|
1245
|
+
)
|
1246
|
+
grpc_client = await self.core()
|
1247
|
+
decoder = grpc_client.get_decoder(SubscribeOrderflowRequest)
|
1248
|
+
stub = grpc_client.channel.unary_stream(
|
1249
|
+
SubscribeOrderflowRequest.get_route(),
|
1250
|
+
request_serializer=grpc_client.encoder().encode,
|
1251
|
+
response_deserializer=decoder.decode,
|
948
1252
|
)
|
949
|
-
|
1253
|
+
call = stub(request, metadata=(("authorization", f"Bearer {grpc_client.jwt}"),))
|
1254
|
+
async for update in call:
|
1255
|
+
yield update
|
950
1256
|
|
951
1257
|
# ------------------------------------------------------------
|
952
|
-
# Order
|
1258
|
+
# Order entry
|
953
1259
|
# ------------------------------------------------------------
|
954
1260
|
|
955
|
-
async def
|
1261
|
+
async def place_limit_order(
|
956
1262
|
self,
|
957
1263
|
*,
|
958
1264
|
id: Optional[str] = None,
|
959
|
-
symbol: TradableProduct,
|
1265
|
+
symbol: TradableProduct | str,
|
960
1266
|
execution_venue: Optional[str],
|
961
1267
|
odir: OrderDir,
|
962
1268
|
quantity: Decimal,
|
@@ -1050,7 +1356,7 @@ class AsyncClient:
|
|
1050
1356
|
self,
|
1051
1357
|
*,
|
1052
1358
|
id: Optional[str] = None,
|
1053
|
-
symbol: TradableProduct,
|
1359
|
+
symbol: TradableProduct | str,
|
1054
1360
|
execution_venue: str,
|
1055
1361
|
odir: OrderDir,
|
1056
1362
|
quantity: Decimal,
|
@@ -1078,36 +1384,33 @@ class AsyncClient:
|
|
1078
1384
|
If the order is rejected, the order.reject_reason and order.reject_message will be set
|
1079
1385
|
"""
|
1080
1386
|
|
1081
|
-
|
1082
|
-
|
1083
|
-
symbol=symbol, venue=execution_venue
|
1084
|
-
)
|
1085
|
-
if bbo_snapshot is None:
|
1387
|
+
ticker = await self.get_ticker(symbol, execution_venue)
|
1388
|
+
if ticker is None:
|
1086
1389
|
raise ValueError(
|
1087
|
-
f"Failed to send market order with reason: no
|
1390
|
+
f"Failed to send market order with reason: no ticker for {symbol}"
|
1088
1391
|
)
|
1089
1392
|
|
1090
1393
|
price_band = price_band_pairs.get(symbol, None)
|
1091
1394
|
|
1092
1395
|
if odir == OrderDir.BUY:
|
1093
|
-
if
|
1396
|
+
if ticker.ask_price is None:
|
1094
1397
|
raise ValueError(
|
1095
1398
|
f"Failed to send market order with reason: no ask price for {symbol}"
|
1096
1399
|
)
|
1097
|
-
limit_price =
|
1400
|
+
limit_price = ticker.ask_price * (1 + fraction_through_market)
|
1098
1401
|
|
1099
|
-
if price_band and
|
1100
|
-
price_band_reference_price =
|
1402
|
+
if price_band and ticker.last_price:
|
1403
|
+
price_band_reference_price = ticker.last_price + price_band
|
1101
1404
|
limit_price = min(limit_price, price_band_reference_price)
|
1102
1405
|
|
1103
1406
|
else:
|
1104
|
-
if
|
1407
|
+
if ticker.bid_price is None:
|
1105
1408
|
raise ValueError(
|
1106
1409
|
f"Failed to send market order with reason: no bid price for {symbol}"
|
1107
1410
|
)
|
1108
|
-
limit_price =
|
1109
|
-
if price_band and
|
1110
|
-
price_band_reference_price =
|
1411
|
+
limit_price = ticker.bid_price * (1 - fraction_through_market)
|
1412
|
+
if price_band and ticker.last_price:
|
1413
|
+
price_band_reference_price = ticker.last_price - price_band
|
1111
1414
|
limit_price = min(limit_price, price_band_reference_price)
|
1112
1415
|
|
1113
1416
|
# Conservatively round price to nearest tick
|
@@ -1125,7 +1428,7 @@ class AsyncClient:
|
|
1125
1428
|
):
|
1126
1429
|
limit_price = tick_round_method(limit_price, tick_size)
|
1127
1430
|
|
1128
|
-
return await self.
|
1431
|
+
return await self.place_limit_order(
|
1129
1432
|
id=id,
|
1130
1433
|
symbol=symbol,
|
1131
1434
|
execution_venue=execution_venue,
|