architect-py 3.2.1__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 +887 -575
- architect_py/client.py +36 -33
- 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 +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 +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 +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_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 +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_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 +293 -223
- architect_py/grpc/resolve_endpoint.py +67 -0
- architect_py/{grpc_client/grpc_server.py → grpc/server.py} +13 -9
- architect_py/grpc/utils.py +32 -0
- architect_py/tests/conftest.py +86 -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 +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.1.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 -52
- 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/__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.0b1.dist-info/licenses}/LICENSE +0 -0
architect_py/async_client.py
CHANGED
@@ -1,208 +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 create 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
|
210
|
+
|
211
|
+
async def discover_marketdata(self):
|
212
|
+
"""
|
213
|
+
Load marketdata endpoints from the server config.
|
214
|
+
|
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.
|
220
|
+
"""
|
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
|
201
320
|
|
202
321
|
# ------------------------------------------------------------
|
203
322
|
# Symbology
|
204
323
|
# ------------------------------------------------------------
|
205
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
|
+
|
206
343
|
async def search_symbols(
|
207
344
|
self,
|
208
345
|
search_string: Optional[str] = None,
|
@@ -211,77 +348,70 @@ class AsyncClient:
|
|
211
348
|
limit: int = 20,
|
212
349
|
) -> List[TradableProduct]:
|
213
350
|
"""
|
214
|
-
Search for
|
351
|
+
Search for tradable products on Architect.
|
215
352
|
|
216
353
|
Args:
|
217
|
-
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.
|
218
356
|
Examples: "ES", "NQ", "GC"
|
219
357
|
execution_venue: the execution venue to search in
|
220
358
|
Examples: "CME"
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
limit=limit,
|
230
|
-
)
|
231
|
-
).search_symbols
|
232
|
-
|
233
|
-
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
|
234
367
|
|
235
368
|
async def get_product_info(self, symbol: str) -> Optional[ProductInfoFields]:
|
236
369
|
"""
|
237
|
-
Get
|
370
|
+
Get information about a product, e.g. product_type, underlying, multiplier.
|
238
371
|
|
239
372
|
Args:
|
240
373
|
symbol: the symbol to get information for
|
374
|
+
|
241
375
|
Returns:
|
242
|
-
|
376
|
+
None if the symbol does not exist
|
243
377
|
"""
|
244
|
-
|
245
|
-
return
|
378
|
+
res = await self.graphql_client.get_product_info_query(symbol)
|
379
|
+
return res.product_info
|
246
380
|
|
247
|
-
@functools.lru_cache
|
248
381
|
async def get_product_infos(
|
249
382
|
self, symbols: Optional[list[str]]
|
250
383
|
) -> Sequence[ProductInfoFields]:
|
251
384
|
"""
|
252
|
-
Get
|
385
|
+
Get information about products, e.g. product_type, underlying, multiplier.
|
253
386
|
|
254
387
|
Args:
|
255
|
-
symbols: the symbols to get information for
|
256
|
-
Returns:
|
257
|
-
a list of ProductInfoFields
|
388
|
+
symbols: the symbols to get information for, or None for all symbols
|
258
389
|
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
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.
|
263
394
|
"""
|
264
|
-
|
265
|
-
return
|
395
|
+
res = await self.graphql_client.get_product_infos_query(symbols)
|
396
|
+
return res.product_infos
|
266
397
|
|
267
|
-
@functools.lru_cache
|
268
398
|
async def get_execution_info(
|
269
|
-
self, symbol: TradableProduct, execution_venue: str
|
399
|
+
self, symbol: TradableProduct | str, execution_venue: str
|
270
400
|
) -> Optional[ExecutionInfoFields]:
|
271
401
|
"""
|
272
|
-
Get
|
402
|
+
Get information about tradable product execution, e.g. tick_size,
|
403
|
+
step_size, margins.
|
273
404
|
|
274
405
|
Args:
|
275
406
|
symbol: the symbol to get execution information for
|
276
407
|
execution_venue: the execution venue e.g. "CME"
|
277
408
|
|
278
409
|
Returns:
|
279
|
-
|
280
|
-
|
410
|
+
None if the symbol doesn't exist
|
281
411
|
"""
|
282
412
|
try:
|
283
413
|
execution_info = await self.graphql_client.get_execution_info_query(
|
284
|
-
symbol, execution_venue
|
414
|
+
TradableProduct(symbol), execution_venue
|
285
415
|
)
|
286
416
|
return execution_info.execution_info
|
287
417
|
except GraphQLClientGraphQLMultiError:
|
@@ -295,59 +425,60 @@ class AsyncClient:
|
|
295
425
|
execution_venue: Optional[str] = None,
|
296
426
|
) -> Sequence[ExecutionInfoFields]:
|
297
427
|
"""
|
298
|
-
Get
|
428
|
+
Get information about tradable product execution, e.g. tick_size,
|
429
|
+
step_size, margins, for many symbols.
|
299
430
|
|
300
431
|
Args:
|
301
|
-
symbols: the symbols to get execution information for
|
432
|
+
symbols: the symbols to get execution information for, or None for all symbols
|
302
433
|
execution_venue: the execution venue e.g. "CME"
|
303
434
|
|
304
435
|
Returns:
|
305
|
-
|
306
|
-
|
307
|
-
|
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.
|
308
439
|
"""
|
309
|
-
|
440
|
+
res = await self.graphql_client.get_execution_infos_query(
|
310
441
|
symbols, execution_venue
|
311
442
|
)
|
312
|
-
return
|
443
|
+
return res.execution_infos
|
313
444
|
|
314
445
|
async def get_cme_first_notice_date(self, symbol: str) -> Optional[date]:
|
315
446
|
"""
|
447
|
+
@deprecated(reason="Use get_product_info instead; first_notice_date is now a field")
|
448
|
+
|
316
449
|
Get the first notice date for a CME future.
|
317
450
|
|
318
451
|
Args:
|
319
452
|
symbol: the symbol to get the first notice date for a CME future
|
320
453
|
|
321
454
|
Returns:
|
322
|
-
|
455
|
+
The first notice date as a date object if it exists
|
323
456
|
"""
|
324
|
-
|
325
|
-
if
|
457
|
+
res = await self.graphql_client.get_first_notice_date_query(symbol)
|
458
|
+
if res is None or res.product_info is None:
|
326
459
|
return None
|
327
|
-
return
|
460
|
+
return res.product_info.first_notice_date
|
328
461
|
|
329
|
-
async def
|
462
|
+
async def get_futures_series(self, series_symbol: str) -> list[str]:
|
330
463
|
"""
|
331
|
-
|
464
|
+
List all futures in a given series.
|
332
465
|
|
333
466
|
Args:
|
334
|
-
series_symbol: the
|
335
|
-
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
|
336
469
|
Returns:
|
337
|
-
|
470
|
+
List of futures products
|
338
471
|
"""
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
futures_series = await self.graphql_client.get_future_series_query(
|
344
|
-
series_symbol
|
345
|
-
)
|
346
|
-
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
|
347
476
|
|
348
477
|
@staticmethod
|
349
|
-
def get_expiration_from_CME_name(name: str) -> date:
|
478
|
+
def get_expiration_from_CME_name(name: str) -> Optional[date]:
|
350
479
|
"""
|
480
|
+
@deprecated(reason="Use utils.symbol_parsing.nominative_expiration instead")
|
481
|
+
|
351
482
|
Get the expiration date from a CME future name.
|
352
483
|
|
353
484
|
Args:
|
@@ -356,12 +487,13 @@ class AsyncClient:
|
|
356
487
|
Returns:
|
357
488
|
the expiration date as a date object
|
358
489
|
"""
|
359
|
-
|
360
|
-
return datetime.strptime(d, "%Y%m%d").date()
|
490
|
+
return nominative_expiration(name)
|
361
491
|
|
362
492
|
async def get_cme_futures_series(self, series: str) -> list[tuple[date, str]]:
|
363
493
|
"""
|
364
|
-
|
494
|
+
@deprecated(reason="Use get_futures_series instead")
|
495
|
+
|
496
|
+
List all futures in a given CME series.
|
365
497
|
|
366
498
|
Args:
|
367
499
|
series: the series to get the futures for
|
@@ -377,33 +509,28 @@ class AsyncClient:
|
|
377
509
|
...
|
378
510
|
]
|
379
511
|
"""
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
]
|
387
|
-
|
388
|
-
filtered_markets.sort(key=lambda x: x[0])
|
389
|
-
|
390
|
-
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
|
391
520
|
|
392
521
|
async def get_cme_future_from_root_month_year(
|
393
522
|
self, root: str, month: int, year: int
|
394
523
|
) -> str:
|
395
524
|
"""
|
396
525
|
Get the symbol for a CME future from the root, month, and year.
|
526
|
+
This is a simple wrapper around search_symbols.
|
397
527
|
|
398
528
|
Args:
|
399
529
|
root: the root symbol for the future e.g. "ES"
|
400
530
|
month: the month of the future
|
401
531
|
year: the year of the future
|
402
532
|
Returns:
|
403
|
-
|
404
|
-
|
405
|
-
Errors if the result is not unique
|
406
|
-
This is a simple wrapper around search_symbols
|
533
|
+
The future symbol if it exists and is unique.
|
407
534
|
"""
|
408
535
|
[market] = [
|
409
536
|
market
|
@@ -413,23 +540,393 @@ class AsyncClient:
|
|
413
540
|
)
|
414
541
|
if market.startswith(f"{root} {year}{month:02d}")
|
415
542
|
]
|
416
|
-
|
417
543
|
return market
|
418
544
|
|
419
545
|
# ------------------------------------------------------------
|
420
|
-
#
|
546
|
+
# Marketdata
|
421
547
|
# ------------------------------------------------------------
|
422
548
|
|
423
|
-
async def
|
549
|
+
async def get_market_status(
|
550
|
+
self, symbol: TradableProduct | str, venue: Venue
|
551
|
+
) -> MarketStatus:
|
424
552
|
"""
|
425
|
-
|
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
|
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.
|
426
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
|
427
575
|
Returns:
|
428
|
-
|
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.
|
429
918
|
"""
|
430
|
-
|
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)
|
431
926
|
|
432
|
-
|
927
|
+
# ------------------------------------------------------------
|
928
|
+
# Portfolio management
|
929
|
+
# ------------------------------------------------------------
|
433
930
|
|
434
931
|
async def list_accounts(self) -> Sequence[AccountWithPermissionsFields]:
|
435
932
|
"""
|
@@ -437,23 +934,35 @@ class AsyncClient:
|
|
437
934
|
|
438
935
|
Returns:
|
439
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
|
440
938
|
(use who_am_i to get the user_id / email)
|
441
939
|
"""
|
442
|
-
|
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)
|
443
947
|
return accounts.accounts
|
948
|
+
"""
|
444
949
|
|
445
950
|
async def get_account_summary(self, account: str) -> AccountSummaryFields:
|
446
951
|
"""
|
447
|
-
|
952
|
+
Get account summary, including balances, positions, pnls, etc.
|
448
953
|
|
449
954
|
Args:
|
450
|
-
account:
|
451
|
-
|
452
|
-
|
453
|
-
|
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
|
454
965
|
"""
|
455
|
-
summary = await self.graphql_client.get_account_summary_query(account=account)
|
456
|
-
return summary.account_summary
|
457
966
|
|
458
967
|
async def get_account_summaries(
|
459
968
|
self,
|
@@ -461,21 +970,31 @@ class AsyncClient:
|
|
461
970
|
trader: Optional[str] = None,
|
462
971
|
) -> Sequence[AccountSummaryFields]:
|
463
972
|
"""
|
464
|
-
|
973
|
+
Get account summaries for accounts matching the filters.
|
465
974
|
|
466
975
|
Args:
|
467
|
-
accounts:
|
468
|
-
|
469
|
-
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
|
470
978
|
|
471
|
-
|
472
|
-
Returns:
|
473
|
-
a list of AccountSummary for the accounts
|
979
|
+
If both arguments are given, the union of matching accounts are returned.
|
474
980
|
"""
|
475
|
-
|
981
|
+
res = await self.graphql_client.get_account_summaries_query(
|
476
982
|
trader=trader, accounts=accounts
|
477
983
|
)
|
478
|
-
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
|
+
"""
|
479
998
|
|
480
999
|
async def get_account_history(
|
481
1000
|
self,
|
@@ -484,19 +1003,43 @@ class AsyncClient:
|
|
484
1003
|
to_exclusive: Optional[datetime] = None,
|
485
1004
|
) -> Sequence[AccountSummaryFields]:
|
486
1005
|
"""
|
487
|
-
|
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
|
488
1012
|
|
489
|
-
Returns:
|
490
|
-
a list of AccountSummaryFields for the account for the given dates
|
491
|
-
use timestamp to get the time of the of the summary
|
492
1013
|
"""
|
493
|
-
|
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(
|
494
1035
|
account=account, from_inclusive=from_inclusive, to_exclusive=to_exclusive
|
495
1036
|
)
|
496
|
-
|
1037
|
+
history = await self.grpc_client.request(request)
|
1038
|
+
return history.history
|
1039
|
+
"""
|
497
1040
|
|
498
1041
|
# ------------------------------------------------------------
|
499
|
-
# Order
|
1042
|
+
# Order management
|
500
1043
|
# ------------------------------------------------------------
|
501
1044
|
|
502
1045
|
async def get_open_orders(
|
@@ -519,12 +1062,10 @@ class AsyncClient:
|
|
519
1062
|
symbol: the symbol to get orders for
|
520
1063
|
parent_order_id: the parent order id to get orders for
|
521
1064
|
|
522
|
-
these filters are combinewd via OR statements so if you pass
|
523
|
-
in multiple arguments, it will return the union of the results
|
524
1065
|
Returns:
|
525
|
-
|
1066
|
+
Open orders that match the union of the filters
|
526
1067
|
"""
|
527
|
-
|
1068
|
+
res = await self.graphql_client.get_open_orders_query(
|
528
1069
|
venue=venue,
|
529
1070
|
account=account,
|
530
1071
|
trader=trader,
|
@@ -532,17 +1073,16 @@ class AsyncClient:
|
|
532
1073
|
parent_order_id=parent_order_id,
|
533
1074
|
order_ids=order_ids,
|
534
1075
|
)
|
535
|
-
return
|
1076
|
+
return res.open_orders
|
536
1077
|
|
537
1078
|
async def get_all_open_orders(self) -> Sequence[OrderFields]:
|
538
1079
|
"""
|
539
|
-
|
1080
|
+
@deprecated(reason="Use get_open_orders with no parameters instead")
|
540
1081
|
|
541
|
-
Returns
|
542
|
-
a list of OrderFields of all the open orders for the user
|
1082
|
+
Returns a list of all open orders for the authenticated user.
|
543
1083
|
"""
|
544
|
-
|
545
|
-
return
|
1084
|
+
res = await self.graphql_client.get_open_orders_query()
|
1085
|
+
return res.open_orders
|
546
1086
|
|
547
1087
|
async def get_historical_orders(
|
548
1088
|
self,
|
@@ -554,24 +1094,25 @@ class AsyncClient:
|
|
554
1094
|
parent_order_id: Optional[str] = None,
|
555
1095
|
) -> Sequence[OrderFields]:
|
556
1096
|
"""
|
557
|
-
|
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.
|
558
1101
|
|
559
1102
|
Args:
|
560
1103
|
order_ids: a list of order ids to get
|
561
1104
|
from_inclusive: the start date to get orders for
|
562
1105
|
to_exclusive: the end date to get orders for
|
563
1106
|
venue: the venue to get orders for, e.g. CME
|
564
|
-
account:
|
565
|
-
can be the account id( (a UUID) or the account name (e.g. CQG:00000)
|
1107
|
+
account: account uuid or name
|
566
1108
|
parent_order_id: the parent order id to get orders for
|
567
1109
|
Returns:
|
568
|
-
|
1110
|
+
Historical orders that match the union of the filters.
|
569
1111
|
|
570
|
-
|
571
|
-
|
572
|
-
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.
|
573
1114
|
"""
|
574
|
-
|
1115
|
+
res = await self.graphql_client.get_historical_orders_query(
|
575
1116
|
order_ids=order_ids,
|
576
1117
|
venue=venue,
|
577
1118
|
account=account,
|
@@ -579,54 +1120,41 @@ class AsyncClient:
|
|
579
1120
|
from_inclusive=from_inclusive,
|
580
1121
|
to_exclusive=to_exclusive,
|
581
1122
|
)
|
582
|
-
return
|
1123
|
+
return res.historical_orders
|
583
1124
|
|
584
1125
|
async def get_order(self, order_id: str) -> Optional[OrderFields]:
|
585
1126
|
"""
|
586
|
-
Returns the
|
587
|
-
|
1127
|
+
Returns the specified order. Useful for looking at past sent orders.
|
1128
|
+
Queries open_orders first, then queries historical_orders.
|
588
1129
|
|
589
1130
|
Args:
|
590
1131
|
order_id: the order id to get
|
591
|
-
Returns:
|
592
|
-
the OrderFields object for the order
|
593
|
-
|
594
|
-
Queries open_orders first then queries historical_orders
|
595
1132
|
"""
|
596
|
-
|
597
|
-
|
598
|
-
)
|
599
|
-
|
600
|
-
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:
|
601
1135
|
if open_order.id == order_id:
|
602
1136
|
return open_order
|
603
1137
|
|
604
|
-
|
1138
|
+
res = await self.graphql_client.get_historical_orders_query(
|
605
1139
|
order_ids=[order_id]
|
606
1140
|
)
|
607
|
-
|
608
|
-
|
609
|
-
return historical_orders.historical_orders[0]
|
1141
|
+
if res.historical_orders and len(res.historical_orders) > 0:
|
1142
|
+
return res.historical_orders[0]
|
610
1143
|
|
611
1144
|
async def get_orders(self, order_ids: list[str]) -> list[Optional[OrderFields]]:
|
612
1145
|
"""
|
613
|
-
Returns
|
614
|
-
|
1146
|
+
Returns the specified orders. Useful for looking at past sent orders.
|
1147
|
+
Plural form of get_order.
|
615
1148
|
|
616
1149
|
Args:
|
617
1150
|
order_ids: a list of order ids to get
|
618
|
-
Returns:
|
619
|
-
a list of OrderFields objects for the orders
|
620
|
-
|
621
|
-
Plural form of get_order
|
622
1151
|
"""
|
623
1152
|
orders_dict: dict[str, Optional[OrderFields]] = {
|
624
1153
|
order_id: None for order_id in order_ids
|
625
1154
|
}
|
626
1155
|
|
627
|
-
|
628
|
-
|
629
|
-
).open_orders
|
1156
|
+
res = await self.graphql_client.get_open_orders_query(order_ids=order_ids)
|
1157
|
+
open_orders = res.open_orders
|
630
1158
|
for open_order in open_orders:
|
631
1159
|
orders_dict[open_order.id] = open_order
|
632
1160
|
|
@@ -634,11 +1162,10 @@ class AsyncClient:
|
|
634
1162
|
order_id for order_id in order_ids if orders_dict[order_id] is None
|
635
1163
|
]
|
636
1164
|
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
).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
|
642
1169
|
for historical_order in historical_orders:
|
643
1170
|
orders_dict[historical_order.id] = historical_order
|
644
1171
|
|
@@ -646,310 +1173,96 @@ class AsyncClient:
|
|
646
1173
|
|
647
1174
|
async def get_fills(
|
648
1175
|
self,
|
649
|
-
from_inclusive: Optional[datetime],
|
650
|
-
to_exclusive: Optional[datetime],
|
1176
|
+
from_inclusive: Optional[datetime] = None,
|
1177
|
+
to_exclusive: Optional[datetime] = None,
|
651
1178
|
venue: Optional[str] = None,
|
652
1179
|
account: Optional[str] = None,
|
653
1180
|
order_id: Optional[str] = None,
|
654
1181
|
) -> GetFillsQueryFolioHistoricalFills:
|
655
1182
|
"""
|
656
|
-
Returns
|
1183
|
+
Returns all fills matching the given filters.
|
657
1184
|
|
658
1185
|
Args:
|
659
1186
|
from_inclusive: the start date to get fills for
|
660
1187
|
to_exclusive: the end date to get fills for
|
661
1188
|
venue: the venue to get fills for, e.g. "CME"
|
662
|
-
account:
|
663
|
-
can be the account id( (a UUID) or the account name (e.g. CQG:00000)
|
1189
|
+
account: account uuid or name
|
664
1190
|
order_id: the order id to get fills for
|
665
|
-
Returns:
|
666
|
-
a list of GetFillsQueryFolioHistoricalFills
|
667
1191
|
"""
|
668
|
-
|
1192
|
+
res = await self.graphql_client.get_fills_query(
|
669
1193
|
venue, account, order_id, from_inclusive, to_exclusive
|
670
1194
|
)
|
671
|
-
return
|
672
|
-
|
673
|
-
# ------------------------------------------------------------
|
674
|
-
# Market Data
|
675
|
-
# ------------------------------------------------------------
|
676
|
-
|
677
|
-
async def get_market_status(
|
678
|
-
self, symbol: TradableProduct, venue: str
|
679
|
-
) -> MarketStatusFields:
|
680
|
-
"""
|
681
|
-
Returns market status for symbol (ie if it is quoting and trading).
|
682
|
-
|
683
|
-
Args:
|
684
|
-
symbol: the symbol to get the market status for, e.g. "ES 20250321 CME Future/USD"
|
685
|
-
venue: the venue that the symbol is traded at, e.g. CME
|
686
|
-
Returns:
|
687
|
-
MarketStatusFields for the symbol
|
688
|
-
"""
|
689
|
-
market_status = await self.graphql_client.get_market_status_query(symbol, venue)
|
690
|
-
return market_status.market_status
|
1195
|
+
return res.historical_fills
|
691
1196
|
|
692
|
-
async def
|
693
|
-
self, symbol: TradableProduct, venue: str
|
694
|
-
) -> MarketTickerFields:
|
695
|
-
"""
|
696
|
-
This is an alias for l1_book_snapshot.
|
697
|
-
|
698
|
-
Args:
|
699
|
-
symbol: the symbol to get the market snapshot for, e.g. "ES 20250321 CME Future/USD"
|
700
|
-
venue: the venue that the symbol is traded at, e.g. CME
|
701
|
-
Returns:
|
702
|
-
MarketTickerFields for the symbol
|
703
|
-
"""
|
704
|
-
return await self.get_l1_book_snapshot(symbol=symbol, venue=venue)
|
705
|
-
|
706
|
-
async def get_market_snapshots(
|
707
|
-
self, symbols: list[TradableProduct], venue: str
|
708
|
-
) -> Sequence[MarketTickerFields]:
|
709
|
-
"""
|
710
|
-
This is an alias for l1_book_snapshot.
|
711
|
-
|
712
|
-
Args:
|
713
|
-
symbols: the symbols to get the market snapshots for
|
714
|
-
venue: the venue that the symbols are traded at
|
715
|
-
Returns:
|
716
|
-
a list of MarketTickerFields for the symbols
|
717
|
-
"""
|
718
|
-
return await self.get_l1_book_snapshots(
|
719
|
-
venue=venue, symbols=symbols # type: ignore
|
720
|
-
)
|
721
|
-
|
722
|
-
async def get_historical_candles(
|
1197
|
+
async def orderflow(
|
723
1198
|
self,
|
724
|
-
|
725
|
-
|
726
|
-
start: datetime,
|
727
|
-
end: datetime,
|
728
|
-
) -> HistoricalCandlesResponse:
|
1199
|
+
request_iterator: AsyncIterator[OrderflowRequest],
|
1200
|
+
) -> AsyncIterator[Orderflow]:
|
729
1201
|
"""
|
730
|
-
|
731
|
-
|
732
|
-
Args:
|
733
|
-
symbol: the symbol to get the candles for
|
734
|
-
venue: the venue of the symbol
|
735
|
-
candle_width: the width of the candles
|
736
|
-
start: the start date to get candles for
|
737
|
-
end: the end date to get candles for
|
738
|
-
Returns:
|
739
|
-
a list of CandleFields for the specified candles
|
740
|
-
"""
|
741
|
-
request = HistoricalCandlesRequest(
|
742
|
-
symbol=symbol,
|
743
|
-
candle_width=candle_width,
|
744
|
-
start_date=start,
|
745
|
-
end_date=end,
|
746
|
-
)
|
1202
|
+
A two-way channel for both order entry and listening to order updates (fills, acks, outs, etc.).
|
747
1203
|
|
748
|
-
|
1204
|
+
This is considered the most efficient way to trade in this SDK.
|
749
1205
|
|
750
|
-
|
751
|
-
|
752
|
-
symbol: str,
|
753
|
-
venue: str,
|
754
|
-
) -> MarketTickerFields:
|
755
|
-
"""
|
756
|
-
Gets the L1 book snapshot for a symbol.
|
1206
|
+
Example:
|
1207
|
+
See test_orderflow.py for an example.
|
757
1208
|
|
758
|
-
|
759
|
-
symbol: the symbol to get the l1 book snapshot for
|
760
|
-
venue: the venue that the symbol is traded at
|
761
|
-
Returns:
|
762
|
-
MarketTickerFields for the symbol
|
1209
|
+
This WILL block the event loop until the stream is closed.
|
763
1210
|
"""
|
764
|
-
|
765
|
-
|
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,
|
766
1217
|
)
|
767
|
-
|
768
|
-
|
769
|
-
async def get_l1_book_snapshots(
|
770
|
-
self, symbols: list[str], venue: str
|
771
|
-
) -> Sequence[MarketTickerFields]:
|
772
|
-
"""
|
773
|
-
Gets the L1 book snapshots for a list of symbols.
|
774
|
-
|
775
|
-
Args:
|
776
|
-
symbols: the symbols to get the l1 book snapshots for
|
777
|
-
venue: the venue that the symbols are traded at
|
778
|
-
Returns:
|
779
|
-
a list of MarketTickerFields for the symbols
|
780
|
-
"""
|
781
|
-
snapshot = await self.graphql_client.get_l_1_book_snapshots_query(
|
782
|
-
venue=venue, symbols=symbols
|
1218
|
+
call = stub(
|
1219
|
+
request_iterator, metadata=(("authorization", f"Bearer {grpc_client.jwt}"),)
|
783
1220
|
)
|
784
|
-
|
785
|
-
|
786
|
-
async def get_l2_book_snapshot(self, symbol: str, venue: str) -> L2BookFields:
|
787
|
-
"""
|
788
|
-
Gets the L2 book snapshot for a symbol.
|
1221
|
+
async for update in call:
|
1222
|
+
yield update
|
789
1223
|
|
790
|
-
|
791
|
-
symbol: the symbol to get the l2 book snapshot for
|
792
|
-
venue: the venue that the symbol is traded at
|
793
|
-
Returns:
|
794
|
-
L2BookFields for the symbol
|
795
|
-
|
796
|
-
Note: this does NOT update, it is a snapshot at a given time
|
797
|
-
For an object that updates, use subscribe_l2_book
|
798
|
-
"""
|
799
|
-
l2_book = await self.graphql_client.get_l_2_book_snapshot_query(
|
800
|
-
symbol=symbol, venue=venue
|
801
|
-
)
|
802
|
-
return l2_book.l_2_book_snapshot
|
803
|
-
|
804
|
-
async def subscribe_l1_book_stream(
|
805
|
-
self, symbols: list[TradableProduct], venue: str
|
806
|
-
) -> AsyncIterator[L1BookSnapshot]:
|
807
|
-
"""
|
808
|
-
Subscribe to the stream of L1BookSnapshots for a symbol.
|
809
|
-
|
810
|
-
Args:
|
811
|
-
symbol: the symbol to subscribe to
|
812
|
-
venue: the venue to subscribe to
|
813
|
-
Returns:
|
814
|
-
an async iterator that yields L1BookSnapshot, representing the l1 book updates
|
815
|
-
"""
|
816
|
-
async for snapshot in await self.grpc_client.subscribe_l1_books_stream(
|
817
|
-
symbols=[str(s) for s in symbols]
|
818
|
-
):
|
819
|
-
yield snapshot
|
820
|
-
|
821
|
-
async def subscribe_l2_book_stream(
|
822
|
-
self, symbol: TradableProduct, venue: str
|
823
|
-
) -> AsyncIterator[L2BookUpdate]:
|
824
|
-
"""
|
825
|
-
Subscribe to the stream of L2BookUpdates for a symbol.
|
826
|
-
|
827
|
-
IMPORTANT: note that the Snapshot is a different type than
|
828
|
-
L2BookSnapshot
|
829
|
-
Args:
|
830
|
-
symbol: the symbol to subscribe to
|
831
|
-
venue: the venue to subscribe to
|
832
|
-
Returns:
|
833
|
-
an async iterator that yields L2BookFields
|
834
|
-
L2BookFields is either a Snapshot or a Diff
|
835
|
-
See the grpc_client code for how to handle the different types
|
836
|
-
"""
|
837
|
-
async for snapshot in self.grpc_client.subscribe_l2_books_stream(
|
838
|
-
symbol=symbol, venue=venue
|
839
|
-
):
|
840
|
-
yield snapshot
|
841
|
-
|
842
|
-
async def subscribe_l1_book(
|
843
|
-
self, symbols: list[TradableProduct]
|
844
|
-
) -> list[L1BookSnapshot]:
|
845
|
-
"""
|
846
|
-
Returns a L1BookSnapshot object that is constantly updating in the background.
|
847
|
-
|
848
|
-
Args:
|
849
|
-
symbols: the symbols to subscribe to
|
850
|
-
Return:
|
851
|
-
a list of L1BookSnapshot objects that are constantly updating in the background
|
852
|
-
For the duration of the program, the client will be subscribed to the stream
|
853
|
-
and be updating the L1BookSnapshot.
|
854
|
-
|
855
|
-
IMPORTANT: The L1BookSnapshot will be initialized with
|
856
|
-
a timestamp (field tn and ts) of 0
|
857
|
-
along with None for bid and ask
|
858
|
-
|
859
|
-
The reference to the object should be kept, but can also be referenced via
|
860
|
-
client.grpc_client.l1_books.get(symbol)
|
861
|
-
|
862
|
-
If you want direct access to the stream to do on_update type code, you can
|
863
|
-
call client.grpc_client.stream_l1_books
|
864
|
-
"""
|
865
|
-
books = self.grpc_client.initialize_l1_books(symbols)
|
866
|
-
asyncio.create_task(self.grpc_client.watch_l1_books(symbols=symbols))
|
867
|
-
i = 0
|
868
|
-
while not all(book.ts > 0 for book in books) and i < 10:
|
869
|
-
await asyncio.sleep(0.2)
|
870
|
-
i += 1
|
871
|
-
if i == 10:
|
872
|
-
raise ValueError(
|
873
|
-
f"Could not get L1 books for {symbols}. Check if market is quoting via client.get_market_status."
|
874
|
-
)
|
875
|
-
|
876
|
-
return books
|
877
|
-
|
878
|
-
async def subscribe_l2_book(
|
1224
|
+
async def stream_orderflow(
|
879
1225
|
self,
|
880
|
-
|
881
|
-
|
882
|
-
|
1226
|
+
account: Optional[grpc_definitions.AccountIdOrName] = None,
|
1227
|
+
execution_venue: Optional[str] = None,
|
1228
|
+
trader: Optional[grpc_definitions.TraderIdOrEmail] = None,
|
1229
|
+
) -> AsyncIterator[Orderflow]:
|
883
1230
|
"""
|
884
|
-
|
1231
|
+
A stream for listening to order updates (fills, acks, outs, etc.).
|
885
1232
|
|
886
|
-
|
887
|
-
|
888
|
-
|
889
|
-
|
890
|
-
|
891
|
-
|
892
|
-
|
893
|
-
IMPORTANT: The LBBookSnapshot will be initialized with
|
894
|
-
a timestamp (field tn and ts) of 0
|
895
|
-
along with None for bid and ask
|
896
|
-
|
897
|
-
The reference to the object should be kept, but can also be referenced via
|
898
|
-
client.grpc_client.l1_books.get(symbol)
|
899
|
-
|
900
|
-
If you want direct access to the stream to do on_update type code, you can
|
901
|
-
call client.grpc_client.stream_l2_book
|
902
|
-
"""
|
903
|
-
book = self.grpc_client.initialize_l2_book(symbol, venue)
|
904
|
-
asyncio.create_task(self.grpc_client.watch_l2_book(symbol, venue))
|
905
|
-
i = 0
|
906
|
-
while book.ts == 0 and i < 10:
|
907
|
-
await asyncio.sleep(0.2)
|
908
|
-
i += 1
|
909
|
-
if book.ts == 0:
|
910
|
-
if venue:
|
911
|
-
market_status = await self.get_market_status(symbol, venue)
|
912
|
-
if not market_status.is_quoting:
|
913
|
-
raise ValueError(
|
914
|
-
f"Market {symbol} is currently closed, cannot get L2."
|
915
|
-
)
|
916
|
-
raise ValueError(f"Could not get L2 book for {symbol}.")
|
917
|
-
|
918
|
-
return book
|
1233
|
+
Example:
|
1234
|
+
```python
|
1235
|
+
request = SubscribeOrderflowRequest.new()
|
1236
|
+
async for of in client.subscribe_orderflow_stream(request):
|
1237
|
+
print(of)
|
1238
|
+
```
|
919
1239
|
|
920
|
-
|
921
|
-
self, symbol: TradableProduct, venue: Optional[str]
|
922
|
-
) -> AsyncIterator[Trade]:
|
923
|
-
"""
|
924
|
-
Subscribe to a stream of trades for a symbol.
|
1240
|
+
This WILL block the event loop until the stream is closed.
|
925
1241
|
"""
|
926
|
-
|
927
|
-
|
928
|
-
|
929
|
-
|
930
|
-
self
|
931
|
-
|
932
|
-
|
933
|
-
|
934
|
-
|
935
|
-
|
936
|
-
Subscribe to a stream of candles for a symbol.
|
937
|
-
"""
|
938
|
-
request = SubscribeCandlesRequest(
|
939
|
-
symbol=str(symbol),
|
940
|
-
venue=venue,
|
941
|
-
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,
|
942
1252
|
)
|
943
|
-
|
1253
|
+
call = stub(request, metadata=(("authorization", f"Bearer {grpc_client.jwt}"),))
|
1254
|
+
async for update in call:
|
1255
|
+
yield update
|
944
1256
|
|
945
1257
|
# ------------------------------------------------------------
|
946
|
-
# Order
|
1258
|
+
# Order entry
|
947
1259
|
# ------------------------------------------------------------
|
948
1260
|
|
949
|
-
async def
|
1261
|
+
async def place_limit_order(
|
950
1262
|
self,
|
951
1263
|
*,
|
952
|
-
|
1264
|
+
id: Optional[str] = None,
|
1265
|
+
symbol: TradableProduct | str,
|
953
1266
|
execution_venue: Optional[str],
|
954
1267
|
odir: OrderDir,
|
955
1268
|
quantity: Decimal,
|
@@ -1027,7 +1340,7 @@ class AsyncClient:
|
|
1027
1340
|
quantity,
|
1028
1341
|
order_type,
|
1029
1342
|
time_in_force,
|
1030
|
-
|
1343
|
+
id,
|
1031
1344
|
trader,
|
1032
1345
|
account,
|
1033
1346
|
limit_price,
|
@@ -1042,7 +1355,8 @@ class AsyncClient:
|
|
1042
1355
|
async def send_market_pro_order(
|
1043
1356
|
self,
|
1044
1357
|
*,
|
1045
|
-
|
1358
|
+
id: Optional[str] = None,
|
1359
|
+
symbol: TradableProduct | str,
|
1046
1360
|
execution_venue: str,
|
1047
1361
|
odir: OrderDir,
|
1048
1362
|
quantity: Decimal,
|
@@ -1070,36 +1384,33 @@ class AsyncClient:
|
|
1070
1384
|
If the order is rejected, the order.reject_reason and order.reject_message will be set
|
1071
1385
|
"""
|
1072
1386
|
|
1073
|
-
|
1074
|
-
|
1075
|
-
symbol=symbol, venue=execution_venue
|
1076
|
-
)
|
1077
|
-
if bbo_snapshot is None:
|
1387
|
+
ticker = await self.get_ticker(symbol, execution_venue)
|
1388
|
+
if ticker is None:
|
1078
1389
|
raise ValueError(
|
1079
|
-
f"Failed to send market order with reason: no
|
1390
|
+
f"Failed to send market order with reason: no ticker for {symbol}"
|
1080
1391
|
)
|
1081
1392
|
|
1082
1393
|
price_band = price_band_pairs.get(symbol, None)
|
1083
1394
|
|
1084
1395
|
if odir == OrderDir.BUY:
|
1085
|
-
if
|
1396
|
+
if ticker.ask_price is None:
|
1086
1397
|
raise ValueError(
|
1087
1398
|
f"Failed to send market order with reason: no ask price for {symbol}"
|
1088
1399
|
)
|
1089
|
-
limit_price =
|
1400
|
+
limit_price = ticker.ask_price * (1 + fraction_through_market)
|
1090
1401
|
|
1091
|
-
if price_band and
|
1092
|
-
price_band_reference_price =
|
1402
|
+
if price_band and ticker.last_price:
|
1403
|
+
price_band_reference_price = ticker.last_price + price_band
|
1093
1404
|
limit_price = min(limit_price, price_band_reference_price)
|
1094
1405
|
|
1095
1406
|
else:
|
1096
|
-
if
|
1407
|
+
if ticker.bid_price is None:
|
1097
1408
|
raise ValueError(
|
1098
1409
|
f"Failed to send market order with reason: no bid price for {symbol}"
|
1099
1410
|
)
|
1100
|
-
limit_price =
|
1101
|
-
if price_band and
|
1102
|
-
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
|
1103
1414
|
limit_price = min(limit_price, price_band_reference_price)
|
1104
1415
|
|
1105
1416
|
# Conservatively round price to nearest tick
|
@@ -1117,7 +1428,8 @@ class AsyncClient:
|
|
1117
1428
|
):
|
1118
1429
|
limit_price = tick_round_method(limit_price, tick_size)
|
1119
1430
|
|
1120
|
-
return await self.
|
1431
|
+
return await self.place_limit_order(
|
1432
|
+
id=id,
|
1121
1433
|
symbol=symbol,
|
1122
1434
|
execution_venue=execution_venue,
|
1123
1435
|
odir=odir,
|