architect-py 3.2.2__py3-none-any.whl → 5.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- architect_py/__init__.py +18 -2
- architect_py/async_client.py +1069 -647
- architect_py/client.py +25 -26
- architect_py/client_interface.py +63 -0
- architect_py/common_types/__init__.py +6 -0
- architect_py/common_types/order_dir.py +91 -0
- architect_py/common_types/scalars.py +25 -0
- architect_py/common_types/tradable_product.py +59 -0
- architect_py/graphql_client/__init__.py +2 -0
- architect_py/graphql_client/client.py +3 -6
- architect_py/graphql_client/enums.py +5 -0
- architect_py/graphql_client/fragments.py +3 -6
- architect_py/graphql_client/get_fills_query.py +2 -1
- architect_py/graphql_client/search_symbols_query.py +2 -1
- architect_py/graphql_client/subscribe_orderflow.py +2 -1
- architect_py/graphql_client/subscribe_trades.py +2 -1
- architect_py/grpc/__init__.py +145 -0
- architect_py/grpc/client.py +94 -0
- architect_py/{grpc_client → grpc/models}/Accounts/AccountsRequest.py +6 -3
- architect_py/{grpc_client → grpc/models}/Accounts/AccountsResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Accounts/__init__.py +1 -1
- architect_py/grpc/models/Algo/AlgoOrder.py +114 -0
- architect_py/grpc/models/Algo/AlgoOrderRequest.py +46 -0
- architect_py/grpc/models/Algo/AlgoOrdersRequest.py +72 -0
- architect_py/grpc/models/Algo/AlgoOrdersResponse.py +27 -0
- architect_py/grpc/models/Algo/CreateAlgoOrderRequest.py +56 -0
- architect_py/grpc/models/Algo/PauseAlgoRequest.py +42 -0
- architect_py/grpc/models/Algo/PauseAlgoResponse.py +20 -0
- architect_py/grpc/models/Algo/StartAlgoRequest.py +42 -0
- architect_py/grpc/models/Algo/StartAlgoResponse.py +20 -0
- architect_py/grpc/models/Algo/StopAlgoRequest.py +42 -0
- architect_py/grpc/models/Algo/StopAlgoResponse.py +20 -0
- architect_py/{grpc_client → grpc/models}/Algo/__init__.py +1 -1
- architect_py/grpc/models/Auth/CreateJwtRequest.py +47 -0
- architect_py/grpc/models/Auth/CreateJwtResponse.py +23 -0
- architect_py/{grpc_client/Cpty → grpc/models/Auth}/__init__.py +1 -1
- architect_py/grpc/models/Boss/DepositsRequest.py +40 -0
- architect_py/grpc/models/Boss/DepositsResponse.py +27 -0
- architect_py/grpc/models/Boss/RqdAccountStatisticsRequest.py +42 -0
- architect_py/grpc/models/Boss/RqdAccountStatisticsResponse.py +25 -0
- architect_py/grpc/models/Boss/StatementUrlRequest.py +40 -0
- architect_py/grpc/models/Boss/StatementUrlResponse.py +23 -0
- architect_py/grpc/models/Boss/StatementsRequest.py +40 -0
- architect_py/grpc/models/Boss/StatementsResponse.py +27 -0
- architect_py/grpc/models/Boss/WithdrawalsRequest.py +40 -0
- architect_py/grpc/models/Boss/WithdrawalsResponse.py +27 -0
- architect_py/{grpc_client/Folio → grpc/models/Boss}/__init__.py +1 -1
- architect_py/grpc/models/Core/ConfigRequest.py +37 -0
- architect_py/grpc/models/Core/ConfigResponse.py +25 -0
- architect_py/grpc/models/Core/__init__.py +2 -0
- architect_py/{grpc_client → grpc/models}/Cpty/CptyRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Cpty/CptyResponse.py +3 -3
- architect_py/{grpc_client → grpc/models}/Cpty/CptyStatus.py +1 -1
- architect_py/{grpc_client → grpc/models}/Cpty/CptyStatusRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Cpty/CptysRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Cpty/CptysResponse.py +1 -1
- architect_py/grpc/models/Cpty/__init__.py +2 -0
- architect_py/{grpc_client → grpc/models}/Folio/AccountHistoryRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Folio/AccountHistoryResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Folio/AccountSummariesRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Folio/AccountSummariesResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Folio/AccountSummary.py +1 -1
- architect_py/{grpc_client → grpc/models}/Folio/AccountSummaryRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Folio/HistoricalFillsRequest.py +7 -4
- architect_py/{grpc_client → grpc/models}/Folio/HistoricalFillsResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Folio/HistoricalOrdersRequest.py +3 -3
- architect_py/{grpc_client → grpc/models}/Folio/HistoricalOrdersResponse.py +1 -1
- architect_py/grpc/models/Folio/__init__.py +2 -0
- architect_py/{grpc_client → grpc/models}/Health/HealthCheckRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Health/HealthCheckResponse.py +1 -1
- architect_py/grpc/models/Health/__init__.py +2 -0
- architect_py/{grpc_client → grpc/models}/Marketdata/Candle.py +1 -1
- architect_py/{grpc_client → grpc/models}/Marketdata/HistoricalCandlesRequest.py +11 -8
- architect_py/{grpc_client → grpc/models}/Marketdata/HistoricalCandlesResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Marketdata/L1BookSnapshot.py +52 -5
- architect_py/{grpc_client → grpc/models}/Marketdata/L1BookSnapshotRequest.py +8 -3
- architect_py/{grpc_client → grpc/models}/Marketdata/L1BookSnapshotsRequest.py +6 -3
- architect_py/{grpc_client → grpc/models}/Marketdata/L2BookSnapshot.py +1 -1
- architect_py/{grpc_client → grpc/models}/Marketdata/L2BookSnapshotRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/Liquidation.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/MarketStatus.py +1 -1
- architect_py/{grpc_client → grpc/models}/Marketdata/MarketStatusRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeCandlesRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeCurrentCandlesRequest.py +3 -4
- architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeL1BookSnapshotsRequest.py +6 -3
- architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeL2BookUpdatesRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeLiquidationsRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeManyCandlesRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeTickersRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeTradesRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/Ticker.py +1 -1
- architect_py/{grpc_client → grpc/models}/Marketdata/TickerRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/TickersRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/TickersResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Marketdata/Trade.py +2 -2
- architect_py/grpc/models/Marketdata/__init__.py +2 -0
- architect_py/grpc/models/Oms/Cancel.py +90 -0
- architect_py/{grpc_client → grpc/models}/Oms/CancelAllOrdersRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Oms/CancelAllOrdersResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Oms/CancelOrderRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Oms/OpenOrdersRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Oms/OpenOrdersResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Oms/Order.py +6 -13
- architect_py/{grpc_client → grpc/models}/Oms/PendingCancelsRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Oms/PendingCancelsResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Oms/PlaceOrderRequest.py +16 -23
- architect_py/grpc/models/Oms/__init__.py +2 -0
- architect_py/grpc/models/OptionsMarketdata/OptionsChain.py +30 -0
- architect_py/grpc/models/OptionsMarketdata/OptionsChainGreeks.py +30 -0
- architect_py/grpc/models/OptionsMarketdata/OptionsChainGreeksRequest.py +47 -0
- architect_py/grpc/models/OptionsMarketdata/OptionsChainRequest.py +45 -0
- architect_py/grpc/models/OptionsMarketdata/OptionsExpirations.py +29 -0
- architect_py/grpc/models/OptionsMarketdata/OptionsExpirationsRequest.py +42 -0
- architect_py/grpc/models/OptionsMarketdata/__init__.py +2 -0
- architect_py/{grpc_client → grpc/models}/Orderflow/DropcopyRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Orderflow/OrderflowRequest.py +1 -1
- architect_py/{grpc_client → grpc/models}/Orderflow/SubscribeOrderflowRequest.py +2 -2
- architect_py/grpc/models/Orderflow/__init__.py +2 -0
- architect_py/grpc/models/Symbology/DownloadProductCatalogRequest.py +42 -0
- architect_py/grpc/models/Symbology/DownloadProductCatalogResponse.py +27 -0
- architect_py/grpc/models/Symbology/ExecutionInfoRequest.py +47 -0
- architect_py/grpc/models/Symbology/ExecutionInfoResponse.py +27 -0
- architect_py/{grpc_client → grpc/models}/Symbology/PruneExpiredSymbolsRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Symbology/PruneExpiredSymbolsResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Symbology/SubscribeSymbology.py +1 -1
- architect_py/{grpc_client → grpc/models}/Symbology/SymbologyRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Symbology/SymbologySnapshot.py +7 -2
- architect_py/{grpc_client → grpc/models}/Symbology/SymbologyUpdate.py +9 -2
- architect_py/{grpc_client → grpc/models}/Symbology/SymbolsRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Symbology/SymbolsResponse.py +1 -1
- architect_py/grpc/models/Symbology/UploadProductCatalogRequest.py +49 -0
- architect_py/grpc/models/Symbology/UploadProductCatalogResponse.py +20 -0
- architect_py/{grpc_client → grpc/models}/Symbology/UploadSymbologyRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Symbology/UploadSymbologyResponse.py +1 -1
- architect_py/grpc/models/Symbology/__init__.py +2 -0
- architect_py/grpc/models/__init__.py +2 -0
- architect_py/{grpc_client → grpc/models}/definitions.py +690 -841
- architect_py/grpc/resolve_endpoint.py +70 -0
- architect_py/{grpc_client/grpc_server.py → grpc/server.py} +9 -6
- architect_py/grpc/utils.py +32 -0
- architect_py/internal_utils/__init__.py +0 -0
- architect_py/internal_utils/no_pandas.py +3 -0
- architect_py/tests/conftest.py +91 -87
- architect_py/tests/test_book_building.py +49 -50
- architect_py/tests/test_marketdata.py +168 -0
- architect_py/tests/test_order_entry.py +37 -0
- architect_py/tests/test_orderflow.py +41 -0
- architect_py/tests/test_portfolio_management.py +23 -0
- architect_py/tests/test_rounding.py +28 -28
- architect_py/tests/test_symbology.py +37 -30
- architect_py/utils/nearest_tick.py +2 -5
- architect_py/utils/nearest_tick_2.py +1 -2
- architect_py/utils/orderbook.py +35 -0
- architect_py/utils/pandas.py +44 -0
- architect_py/utils/price_bands.py +0 -3
- architect_py/utils/symbol_parsing.py +29 -0
- architect_py-5.0.0.dist-info/METADATA +54 -0
- architect_py-5.0.0.dist-info/RECORD +214 -0
- {architect_py-3.2.2.dist-info → architect_py-5.0.0.dist-info}/WHEEL +2 -1
- architect_py-5.0.0.dist-info/top_level.txt +4 -0
- examples/__init__.py +0 -0
- examples/book_subscription.py +52 -0
- examples/candles.py +30 -0
- examples/common.py +116 -0
- examples/external_cpty.py +77 -0
- examples/funding_rate_mean_reversion_algo.py +186 -0
- examples/order_sending.py +91 -0
- examples/stream_l1_marketdata.py +25 -0
- examples/stream_l2_marketdata.py +38 -0
- examples/trades.py +21 -0
- examples/tutorial_async.py +86 -0
- examples/tutorial_sync.py +95 -0
- scripts/generate_functions_md.py +166 -0
- scripts/generate_sync_interface.py +226 -0
- scripts/postprocess_grpc.py +604 -0
- scripts/preprocess_grpc_schema.py +708 -0
- templates/exceptions.py +83 -0
- templates/juniper_base_client.py +371 -0
- architect_py/client_protocol.py +0 -53
- architect_py/grpc_client/Algo/AlgoOrderForTwapAlgo.py +0 -61
- architect_py/grpc_client/Algo/CreateAlgoOrderRequestForTwapAlgo.py +0 -59
- architect_py/grpc_client/Algo/ModifyAlgoOrderRequestForTwapAlgo.py +0 -45
- architect_py/grpc_client/Health/__init__.py +0 -2
- architect_py/grpc_client/Marketdata/__init__.py +0 -2
- architect_py/grpc_client/Oms/Cancel.py +0 -42
- architect_py/grpc_client/Oms/__init__.py +0 -2
- architect_py/grpc_client/Orderflow/__init__.py +0 -2
- architect_py/grpc_client/Symbology/__init__.py +0 -2
- architect_py/grpc_client/__init__.py +0 -2
- architect_py/grpc_client/grpc_client.py +0 -413
- architect_py/scalars.py +0 -172
- architect_py/tests/test_accounts.py +0 -31
- architect_py/tests/test_client.py +0 -29
- architect_py/tests/test_grpc_client.py +0 -30
- architect_py/tests/test_order_sending.py +0 -65
- architect_py/tests/test_snapshots.py +0 -52
- architect_py/tests/test_subscriptions.py +0 -126
- architect_py-3.2.2.dist-info/METADATA +0 -191
- architect_py-3.2.2.dist-info/RECORD +0 -148
- /architect_py/{grpc_client → grpc/models}/Marketdata/ArrayOfL1BookSnapshot.py +0 -0
- /architect_py/{grpc_client → grpc/models}/Marketdata/L2BookUpdate.py +0 -0
- /architect_py/{grpc_client → grpc/models}/Marketdata/TickerUpdate.py +0 -0
- /architect_py/{grpc_client → grpc/models}/Orderflow/Dropcopy.py +0 -0
- /architect_py/{grpc_client → grpc/models}/Orderflow/Orderflow.py +0 -0
- {architect_py-3.2.2.dist-info → architect_py-5.0.0.dist-info/licenses}/LICENSE +0 -0
architect_py/async_client.py
CHANGED
@@ -1,216 +1,362 @@
|
|
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, timezone
|
21
5
|
from decimal import Decimal
|
22
|
-
from typing import
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
6
|
+
from typing import (
|
7
|
+
Any,
|
8
|
+
AsyncGenerator,
|
9
|
+
AsyncIterator,
|
10
|
+
List,
|
11
|
+
Literal,
|
12
|
+
Optional,
|
13
|
+
Sequence,
|
14
|
+
Union,
|
15
|
+
overload,
|
27
16
|
)
|
28
17
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
from architect_py.
|
34
|
-
|
18
|
+
import grpc
|
19
|
+
import pandas as pd
|
20
|
+
|
21
|
+
from architect_py.grpc.models.Oms.Cancel import Cancel
|
22
|
+
from architect_py.grpc.models.Oms.CancelAllOrdersRequest import CancelAllOrdersRequest
|
23
|
+
from architect_py.grpc.models.Oms.CancelOrderRequest import CancelOrderRequest
|
24
|
+
from architect_py.grpc.models.Oms.OpenOrdersRequest import OpenOrdersRequest
|
25
|
+
from architect_py.grpc.models.Oms.Order import Order
|
26
|
+
from architect_py.grpc.models.Oms.PlaceOrderRequest import (
|
27
|
+
PlaceOrderRequest,
|
35
28
|
)
|
36
|
-
|
37
|
-
from architect_py.
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
from architect_py.grpc_client.Marketdata.SubscribeCandlesRequest import (
|
42
|
-
SubscribeCandlesRequest,
|
29
|
+
from architect_py.grpc.models.Orderflow.Orderflow import Orderflow
|
30
|
+
from architect_py.grpc.models.Orderflow.OrderflowRequest import (
|
31
|
+
OrderflowRequest,
|
32
|
+
OrderflowRequest_route,
|
33
|
+
OrderflowRequestUnannotatedResponseType,
|
43
34
|
)
|
44
|
-
from architect_py.
|
45
|
-
|
35
|
+
from architect_py.grpc.models.Orderflow.SubscribeOrderflowRequest import (
|
36
|
+
SubscribeOrderflowRequest,
|
46
37
|
)
|
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
38
|
|
39
|
+
from .common_types import OrderDir, TradableProduct, Venue
|
51
40
|
from .graphql_client import GraphQLClient
|
52
|
-
from .graphql_client.
|
53
|
-
OrderType,
|
54
|
-
TimeInForce,
|
55
|
-
)
|
41
|
+
from .graphql_client.exceptions import GraphQLClientGraphQLMultiError
|
56
42
|
from .graphql_client.fragments import (
|
57
|
-
AccountSummaryFields,
|
58
|
-
AccountWithPermissionsFields,
|
59
|
-
CancelFields,
|
60
43
|
ExecutionInfoFields,
|
61
|
-
L2BookFields,
|
62
|
-
MarketStatusFields,
|
63
|
-
MarketTickerFields,
|
64
|
-
OrderFields,
|
65
44
|
ProductInfoFields,
|
66
45
|
)
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
# CreateSmartOrderRouterAlgo,
|
73
|
-
# CreateSpreadAlgo,
|
74
|
-
# CreateSpreadAlgoHedgeMarket,
|
75
|
-
# CreateTimeInForce,
|
76
|
-
# CreateTimeInForceInstruction,
|
77
|
-
# CreateTwapAlgo,
|
78
|
-
# )
|
79
|
-
from .grpc_client import GRPCClient
|
80
|
-
|
46
|
+
from .grpc import *
|
47
|
+
from .grpc.client import GrpcClient
|
48
|
+
from .utils.nearest_tick import TickRoundMethod
|
49
|
+
from .utils.orderbook import update_orderbook_side
|
50
|
+
from .utils.pandas import candles_to_dataframe
|
81
51
|
from .utils.price_bands import price_band_pairs
|
82
|
-
|
83
|
-
logger = logging.getLogger(__name__)
|
52
|
+
from .utils.symbol_parsing import nominative_expiration
|
84
53
|
|
85
54
|
|
86
55
|
class AsyncClient:
|
56
|
+
api_key: Optional[str] = None
|
57
|
+
api_secret: Optional[str] = None
|
58
|
+
paper_trading: bool
|
87
59
|
graphql_client: GraphQLClient
|
88
|
-
|
60
|
+
grpc_core: Optional[GrpcClient] = None
|
61
|
+
grpc_marketdata: dict[Venue, GrpcClient] = {}
|
62
|
+
grpc_hmart: Optional[GrpcClient] = None
|
63
|
+
jwt: str | None = None
|
64
|
+
jwt_expiration: datetime | None = None
|
65
|
+
|
66
|
+
l1_books: dict[Venue, dict[TradableProduct, tuple[L1BookSnapshot, asyncio.Task]]]
|
67
|
+
l2_books: dict[Venue, dict[TradableProduct, tuple[L2BookSnapshot, asyncio.Task]]]
|
89
68
|
|
90
69
|
# ------------------------------------------------------------
|
91
|
-
# Initialization
|
70
|
+
# Initialization and configuration
|
92
71
|
# ------------------------------------------------------------
|
93
72
|
|
94
73
|
@staticmethod
|
95
74
|
async def connect(
|
96
75
|
*,
|
97
|
-
api_key: str,
|
98
|
-
api_secret: str,
|
76
|
+
api_key: Optional[str] = None,
|
77
|
+
api_secret: Optional[str] = None,
|
99
78
|
paper_trading: bool,
|
100
|
-
|
101
|
-
|
102
|
-
_port: Optional[int] = None,
|
79
|
+
endpoint: str = "https://app.architect.co",
|
80
|
+
graphql_port: Optional[int] = None,
|
103
81
|
**kwargs: Any,
|
104
82
|
) -> "AsyncClient":
|
105
83
|
"""
|
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
|
84
|
+
Connect to an Architect installation.
|
114
85
|
|
115
|
-
the API key and secret
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
ValueError: If the API key or secret are not the correct length or contain invalid characters
|
86
|
+
Raises ValueError if the API key and secret are not the correct length or contain invalid characters.
|
87
|
+
"""
|
88
|
+
if paper_trading:
|
89
|
+
COLOR = "\033[30;43m"
|
90
|
+
RESET = "\033[0m"
|
91
|
+
print(f"🧻 {COLOR} YOU ARE IN PAPER TRADING MODE {RESET}")
|
122
92
|
|
123
|
-
|
124
|
-
|
93
|
+
if "grpc_endpoint" in kwargs:
|
94
|
+
logging.warning(
|
95
|
+
"as of v5.0.0: grpc_endpoint parameter is deprecated; ignored"
|
96
|
+
)
|
97
|
+
if "host" in kwargs:
|
98
|
+
logging.warning(
|
99
|
+
"as of v5.0.0: host parameter is deprecated, use endpoint instead; setting endpoint to %s",
|
100
|
+
kwargs["endpoint"],
|
101
|
+
)
|
102
|
+
endpoint = kwargs["endpoint"]
|
125
103
|
|
126
|
-
|
104
|
+
grpc_host, grpc_port, use_ssl = await resolve_endpoint(endpoint)
|
105
|
+
logging.info(
|
106
|
+
f"Resolved endpoint {endpoint}: {grpc_host}:{grpc_port} use_ssl={use_ssl}"
|
107
|
+
)
|
127
108
|
|
128
|
-
|
129
|
-
likely due to the client not being instantiated with the connect method
|
130
|
-
"""
|
109
|
+
# Sanity check paper trading on prod environments
|
131
110
|
if paper_trading:
|
132
|
-
|
133
|
-
|
134
|
-
|
111
|
+
if grpc_host == "app.architect.co" or grpc_host == "staging.architect.co":
|
112
|
+
if grpc_port != 10081:
|
113
|
+
raise ValueError("Wrong gRPC port for paper trading")
|
114
|
+
if graphql_port is not None and graphql_port != 5678:
|
115
|
+
raise ValueError("Wrong GraphQL port for paper trading")
|
135
116
|
|
136
|
-
|
117
|
+
client = AsyncClient(
|
137
118
|
api_key=api_key,
|
138
119
|
api_secret=api_secret,
|
139
|
-
host=host,
|
140
120
|
paper_trading=paper_trading,
|
141
|
-
|
121
|
+
grpc_host=grpc_host,
|
122
|
+
grpc_port=grpc_port,
|
123
|
+
graphql_port=graphql_port,
|
124
|
+
use_ssl=use_ssl,
|
142
125
|
_i_know_what_i_am_doing=True,
|
143
|
-
**kwargs,
|
144
126
|
)
|
145
127
|
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
128
|
+
logging.info("Exchanging credentials...")
|
129
|
+
await client.refresh_jwt()
|
130
|
+
|
131
|
+
logging.info("Discovering marketdata endpoints...")
|
132
|
+
await client.discover_marketdata()
|
133
|
+
|
134
|
+
return client
|
151
135
|
|
152
136
|
def __init__(
|
153
137
|
self,
|
154
138
|
*,
|
155
|
-
api_key: str,
|
156
|
-
api_secret: str,
|
157
|
-
|
158
|
-
|
159
|
-
|
139
|
+
api_key: Optional[str] = None,
|
140
|
+
api_secret: Optional[str] = None,
|
141
|
+
paper_trading: bool,
|
142
|
+
grpc_host: str = "app.architect.co",
|
143
|
+
grpc_port: int,
|
144
|
+
graphql_port: Optional[int] = None,
|
145
|
+
use_ssl: bool = True,
|
160
146
|
_i_know_what_i_am_doing: bool = False,
|
161
|
-
**kwargs: Any,
|
162
147
|
):
|
163
148
|
"""
|
164
|
-
|
165
|
-
|
166
|
-
Use the connect method instead.
|
167
|
-
See self.connect for arg explanations
|
149
|
+
Use AsyncClient.connect instead.
|
168
150
|
"""
|
169
|
-
|
170
151
|
if not _i_know_what_i_am_doing:
|
171
|
-
raise ValueError(
|
172
|
-
"Please use the connect method to create an AsyncClient object."
|
173
|
-
)
|
152
|
+
raise ValueError("Use AsyncClient.connect to create an AsyncClient object.")
|
174
153
|
|
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:
|
184
|
-
raise ValueError(
|
185
|
-
"API key and secret cannot contain spaces, please double check your credentials."
|
186
|
-
)
|
187
|
-
elif len(api_key) != 24 or len(api_secret) != 44:
|
154
|
+
if api_key is not None and not re.match(r"^[a-zA-Z0-9]{24}$", api_key):
|
155
|
+
raise ValueError("API key must be exactly 24 alphanumeric characters")
|
156
|
+
if api_secret is not None and not re.match(
|
157
|
+
r"^[a-zA-Z0-9+\/=]{44}$", api_secret
|
158
|
+
):
|
188
159
|
raise ValueError(
|
189
|
-
"API
|
160
|
+
"API secret must be a Base64-encoded string, 44 characters long"
|
190
161
|
)
|
191
162
|
|
192
|
-
if
|
163
|
+
if graphql_port is None:
|
193
164
|
if paper_trading:
|
194
|
-
|
165
|
+
graphql_port = 5678
|
195
166
|
else:
|
196
|
-
|
167
|
+
graphql_port = 4567
|
197
168
|
|
169
|
+
self.api_key = api_key
|
170
|
+
self.api_secret = api_secret
|
171
|
+
self.paper_trading = paper_trading
|
198
172
|
self.graphql_client = GraphQLClient(
|
199
|
-
|
173
|
+
host=grpc_host,
|
174
|
+
port=graphql_port,
|
175
|
+
use_tls=use_ssl,
|
176
|
+
api_key=api_key,
|
177
|
+
api_secret=api_secret,
|
200
178
|
)
|
179
|
+
self.grpc_core = GrpcClient(host=grpc_host, port=grpc_port, use_ssl=use_ssl)
|
180
|
+
|
181
|
+
async def refresh_jwt(self, force: bool = False):
|
182
|
+
"""
|
183
|
+
Refresh the JWT for the gRPC channel if it's nearing expiration (within 1 minute).
|
184
|
+
If force=True, refresh the JWT unconditionally.
|
185
|
+
|
186
|
+
Query methods on AsyncClient that require auth will call this method internally.
|
187
|
+
"""
|
188
|
+
if not self.api_key or not self.api_secret:
|
189
|
+
raise ValueError("API key and secret not set")
|
190
|
+
if self.grpc_core is None:
|
191
|
+
raise ValueError("gRPC client to Architect not initialized")
|
192
|
+
|
193
|
+
if (
|
194
|
+
force
|
195
|
+
or self.jwt_expiration is None
|
196
|
+
or datetime.now() > self.jwt_expiration - timedelta(minutes=1)
|
197
|
+
):
|
198
|
+
try:
|
199
|
+
req = CreateJwtRequest(api_key=self.api_key, api_secret=self.api_secret)
|
200
|
+
res: CreateJwtResponse = await self.grpc_core.unary_unary(req)
|
201
|
+
self.jwt = res.jwt
|
202
|
+
# CR alee: actually read the JWT to get the expiration time;
|
203
|
+
# for now, we just "know" that the JWTs are granted for an hour
|
204
|
+
self.jwt_expiration = datetime.now() + timedelta(hours=1)
|
205
|
+
except Exception as e:
|
206
|
+
logging.error("Failed to refresh gRPC credentials: %s", e)
|
207
|
+
|
208
|
+
def set_jwt(self, jwt: str | None, jwt_expiration: datetime | None = None):
|
209
|
+
"""
|
210
|
+
Manually set the JWT for gRPC authentication.
|
211
|
+
|
212
|
+
Args:
|
213
|
+
jwt: the JWT to set;
|
214
|
+
None to clear the JWT
|
215
|
+
jwt_expiration: when to expire the JWT
|
216
|
+
"""
|
217
|
+
self.jwt = jwt
|
218
|
+
self.jwt_expiration = jwt_expiration
|
219
|
+
|
220
|
+
async def discover_marketdata(self):
|
221
|
+
"""
|
222
|
+
Load marketdata endpoints from the server config.
|
223
|
+
|
224
|
+
The Architect core is responsible for telling you where to find marketdata as per
|
225
|
+
its configuration. You can also manually set marketdata endpoints by calling
|
226
|
+
set_marketdata directly.
|
227
|
+
|
228
|
+
This method is called on AsyncClient.connect.
|
229
|
+
"""
|
230
|
+
try:
|
231
|
+
grpc_client = await self.core()
|
232
|
+
req = ConfigRequest()
|
233
|
+
res: ConfigResponse = await grpc_client.unary_unary(req)
|
234
|
+
for venue, endpoint in res.marketdata.items():
|
235
|
+
try:
|
236
|
+
grpc_host, grpc_port, use_ssl = await resolve_endpoint(endpoint)
|
237
|
+
logging.info(
|
238
|
+
"Setting marketdata endpoint for %s: %s:%d use_ssl=%s",
|
239
|
+
venue,
|
240
|
+
grpc_host,
|
241
|
+
grpc_port,
|
242
|
+
use_ssl,
|
243
|
+
)
|
244
|
+
self.grpc_marketdata[venue] = GrpcClient(
|
245
|
+
host=grpc_host, port=grpc_port, use_ssl=use_ssl
|
246
|
+
)
|
247
|
+
except Exception as e:
|
248
|
+
logging.error("Failed to set marketdata endpoint: %s", e)
|
249
|
+
except Exception as e:
|
250
|
+
logging.error("Failed to get marketdata config: %s", e)
|
201
251
|
|
202
|
-
async def
|
252
|
+
async def set_marketdata(self, venue: Venue, endpoint: str):
|
203
253
|
"""
|
204
|
-
|
254
|
+
Manually set the marketdata endpoint for a venue.
|
255
|
+
"""
|
256
|
+
try:
|
257
|
+
grpc_host, grpc_port, use_ssl = await resolve_endpoint(endpoint)
|
258
|
+
self.grpc_marketdata[venue] = GrpcClient(
|
259
|
+
host=grpc_host, port=grpc_port, use_ssl=use_ssl
|
260
|
+
)
|
261
|
+
except Exception as e:
|
262
|
+
logging.error("Failed to set marketdata endpoint: %s", e)
|
263
|
+
|
264
|
+
async def marketdata(self, venue: Venue) -> GrpcClient:
|
265
|
+
"""
|
266
|
+
Get the marketdata client for a venue.
|
267
|
+
"""
|
268
|
+
if venue not in self.grpc_marketdata:
|
269
|
+
raise ValueError(f"Marketdata not configured for venue: {venue}")
|
270
|
+
|
271
|
+
await self.refresh_jwt()
|
272
|
+
self.grpc_marketdata[venue].set_jwt(self.jwt)
|
273
|
+
return self.grpc_marketdata[venue]
|
274
|
+
|
275
|
+
async def set_hmart(self, endpoint: str):
|
276
|
+
"""
|
277
|
+
Manually set the hmart (historical marketdata service) endpoint.
|
278
|
+
"""
|
279
|
+
try:
|
280
|
+
grpc_host, grpc_port, use_ssl = await resolve_endpoint(endpoint)
|
281
|
+
logging.info(
|
282
|
+
"Resolved hmart endpoint %s: %s:%d use_ssl=%s",
|
283
|
+
endpoint,
|
284
|
+
grpc_host,
|
285
|
+
grpc_port,
|
286
|
+
use_ssl,
|
287
|
+
)
|
288
|
+
self.grpc_hmart = GrpcClient(
|
289
|
+
host=grpc_host, port=grpc_port, use_ssl=use_ssl
|
290
|
+
)
|
291
|
+
except Exception as e:
|
292
|
+
logging.error("Failed to set hmart endpoint: %s", e)
|
293
|
+
|
294
|
+
async def hmart(self) -> GrpcClient:
|
295
|
+
"""
|
296
|
+
Get the hmart (historical marketdata service) client.
|
297
|
+
"""
|
298
|
+
if self.grpc_hmart is None:
|
299
|
+
# default to historical.marketdata.architect.co
|
300
|
+
await self.set_hmart("https://historical.marketdata.architect.co")
|
205
301
|
|
206
|
-
|
302
|
+
if self.grpc_hmart is None:
|
303
|
+
raise ValueError("hmart client not initialized")
|
304
|
+
|
305
|
+
await self.refresh_jwt()
|
306
|
+
self.grpc_hmart.set_jwt(self.jwt)
|
307
|
+
return self.grpc_hmart
|
308
|
+
|
309
|
+
async def core(self) -> GrpcClient:
|
310
|
+
"""
|
311
|
+
Get the core client.
|
312
|
+
"""
|
313
|
+
if self.grpc_core is None:
|
314
|
+
raise ValueError("gRPC client to Architect not initialized")
|
315
|
+
|
316
|
+
await self.refresh_jwt()
|
317
|
+
self.grpc_core.set_jwt(self.jwt)
|
318
|
+
return self.grpc_core
|
319
|
+
|
320
|
+
async def who_am_i(self) -> tuple[str, str]:
|
321
|
+
"""
|
322
|
+
Gets the user_id and user_email for the user that the API key belongs to.
|
323
|
+
|
324
|
+
Returns:
|
325
|
+
(user_id, user_email)
|
207
326
|
"""
|
208
|
-
|
327
|
+
res = await self.graphql_client.user_id_query()
|
328
|
+
return res.user_id, res.user_email
|
329
|
+
|
330
|
+
def enable_orderflow(self):
|
331
|
+
"""
|
332
|
+
@deprecated(reason="No longer needed; orderflow is enabled by default")
|
333
|
+
"""
|
334
|
+
logging.warning(
|
335
|
+
"as of v5.0.0: enable_orderflow is deprecated; orderflow is enabled by default"
|
336
|
+
)
|
209
337
|
|
210
338
|
# ------------------------------------------------------------
|
211
339
|
# Symbology
|
212
340
|
# ------------------------------------------------------------
|
213
341
|
|
342
|
+
async def list_symbols(self, *, marketdata: Optional[Venue] = None) -> list[str]:
|
343
|
+
"""
|
344
|
+
List all symbols.
|
345
|
+
|
346
|
+
Args:
|
347
|
+
marketdata: query marketdata endpoint for the specified venue directly;
|
348
|
+
If provided, query the venue's marketdata endpoint directly,
|
349
|
+
instead of the Architect core. This is sometimes useful for
|
350
|
+
cross-referencing symbols or checking availability.
|
351
|
+
"""
|
352
|
+
if marketdata is not None:
|
353
|
+
grpc_client = await self.marketdata(marketdata)
|
354
|
+
else:
|
355
|
+
grpc_client = await self.core()
|
356
|
+
req = SymbolsRequest()
|
357
|
+
res: SymbolsResponse = await grpc_client.unary_unary(req)
|
358
|
+
return res.symbols
|
359
|
+
|
214
360
|
async def search_symbols(
|
215
361
|
self,
|
216
362
|
search_string: Optional[str] = None,
|
@@ -219,75 +365,74 @@ class AsyncClient:
|
|
219
365
|
limit: int = 20,
|
220
366
|
) -> List[TradableProduct]:
|
221
367
|
"""
|
222
|
-
Search for
|
368
|
+
Search for tradable products on Architect.
|
223
369
|
|
224
370
|
Args:
|
225
|
-
search_string: a string to search for in the symbol
|
371
|
+
search_string: a string to search for in the symbol
|
372
|
+
Can be "*" for wild card search.
|
226
373
|
Examples: "ES", "NQ", "GC"
|
227
374
|
execution_venue: the execution venue to search in
|
228
375
|
Examples: "CME"
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
limit=limit,
|
238
|
-
)
|
239
|
-
).search_symbols
|
240
|
-
|
241
|
-
return markets
|
376
|
+
"""
|
377
|
+
res = await self.graphql_client.search_symbols_query(
|
378
|
+
search_string=search_string,
|
379
|
+
execution_venue=execution_venue,
|
380
|
+
offset=offset,
|
381
|
+
limit=limit,
|
382
|
+
)
|
383
|
+
return res.search_symbols
|
242
384
|
|
243
385
|
async def get_product_info(self, symbol: str) -> Optional[ProductInfoFields]:
|
244
386
|
"""
|
245
|
-
Get
|
387
|
+
Get information about a product, e.g. product_type, underlying, multiplier.
|
246
388
|
|
247
389
|
Args:
|
248
390
|
symbol: the symbol to get information for
|
391
|
+
the symbol should *not* have a quote,
|
392
|
+
ie "ES 20250620 CME Future" instead of "ES 20250620 CME Future/USD"
|
393
|
+
|
394
|
+
If you used TradableProduct, you can use the base() method to get the symbol
|
395
|
+
|
249
396
|
Returns:
|
250
|
-
|
397
|
+
None if the symbol does not exist
|
251
398
|
"""
|
252
|
-
|
253
|
-
return
|
399
|
+
res = await self.graphql_client.get_product_info_query(symbol)
|
400
|
+
return res.product_info
|
254
401
|
|
255
402
|
async def get_product_infos(
|
256
403
|
self, symbols: Optional[list[str]]
|
257
404
|
) -> Sequence[ProductInfoFields]:
|
258
405
|
"""
|
259
|
-
Get
|
406
|
+
Get information about products, e.g. product_type, underlying, multiplier.
|
260
407
|
|
261
408
|
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.
|
409
|
+
symbols: the symbols to get information for, or None for all symbols
|
268
410
|
|
269
|
-
|
411
|
+
Returns:
|
412
|
+
Product infos for each symbol. Not guaranteed to contain all symbols
|
413
|
+
that were asked for, or in the same order; any duplicates or invalid
|
414
|
+
symbols will be ignored.
|
270
415
|
"""
|
271
|
-
|
272
|
-
return
|
416
|
+
res = await self.graphql_client.get_product_infos_query(symbols)
|
417
|
+
return res.product_infos
|
273
418
|
|
274
419
|
async def get_execution_info(
|
275
|
-
self, symbol: TradableProduct, execution_venue: str
|
420
|
+
self, symbol: TradableProduct | str, execution_venue: str
|
276
421
|
) -> Optional[ExecutionInfoFields]:
|
277
422
|
"""
|
278
|
-
Get
|
423
|
+
Get information about tradable product execution, e.g. tick_size,
|
424
|
+
step_size, margins.
|
279
425
|
|
280
426
|
Args:
|
281
427
|
symbol: the symbol to get execution information for
|
282
428
|
execution_venue: the execution venue e.g. "CME"
|
283
429
|
|
284
430
|
Returns:
|
285
|
-
|
286
|
-
|
431
|
+
None if the symbol doesn't exist
|
287
432
|
"""
|
288
433
|
try:
|
289
434
|
execution_info = await self.graphql_client.get_execution_info_query(
|
290
|
-
symbol, execution_venue
|
435
|
+
TradableProduct(symbol), execution_venue
|
291
436
|
)
|
292
437
|
return execution_info.execution_info
|
293
438
|
except GraphQLClientGraphQLMultiError:
|
@@ -297,63 +442,73 @@ class AsyncClient:
|
|
297
442
|
|
298
443
|
async def get_execution_infos(
|
299
444
|
self,
|
300
|
-
symbols: Optional[list[TradableProduct]],
|
445
|
+
symbols: Optional[list[TradableProduct | str]],
|
301
446
|
execution_venue: Optional[str] = None,
|
302
447
|
) -> Sequence[ExecutionInfoFields]:
|
303
448
|
"""
|
304
|
-
Get
|
449
|
+
Get information about tradable product execution, e.g. tick_size,
|
450
|
+
step_size, margins, for many symbols.
|
305
451
|
|
306
452
|
Args:
|
307
|
-
symbols: the symbols to get execution information for
|
453
|
+
symbols: the symbols to get execution information for, or None for all symbols
|
308
454
|
execution_venue: the execution venue e.g. "CME"
|
309
455
|
|
310
456
|
Returns:
|
311
|
-
|
312
|
-
|
313
|
-
|
457
|
+
Execution infos for each symbol. Not guaranteed to contain all symbols
|
458
|
+
that were asked for, or in the same order; any duplicates or invalid
|
459
|
+
symbols will be ignored.
|
314
460
|
"""
|
315
|
-
|
316
|
-
symbols
|
317
|
-
|
318
|
-
|
461
|
+
if symbols is not None:
|
462
|
+
tps = [TradableProduct(symbol) for symbol in symbols]
|
463
|
+
else:
|
464
|
+
tps = None
|
465
|
+
res = await self.graphql_client.get_execution_infos_query(tps, execution_venue)
|
466
|
+
return res.execution_infos
|
319
467
|
|
320
468
|
async def get_cme_first_notice_date(self, symbol: str) -> Optional[date]:
|
321
469
|
"""
|
470
|
+
@deprecated(reason="Use get_product_info instead; first_notice_date is now a field")
|
471
|
+
|
322
472
|
Get the first notice date for a CME future.
|
323
473
|
|
324
474
|
Args:
|
325
475
|
symbol: the symbol to get the first notice date for a CME future
|
326
476
|
|
327
477
|
Returns:
|
328
|
-
|
478
|
+
The first notice date as a date object if it exists
|
329
479
|
"""
|
330
|
-
|
331
|
-
if
|
480
|
+
res = await self.graphql_client.get_first_notice_date_query(symbol)
|
481
|
+
if res is None or res.product_info is None:
|
332
482
|
return None
|
333
|
-
return
|
483
|
+
return res.product_info.first_notice_date
|
334
484
|
|
335
485
|
async def get_future_series(self, series_symbol: str) -> list[str]:
|
336
486
|
"""
|
337
|
-
|
487
|
+
@deprecated(reason="Use get_futures_series instead")
|
488
|
+
"""
|
489
|
+
futures = await self.get_futures_series(series_symbol)
|
490
|
+
return futures
|
491
|
+
|
492
|
+
async def get_futures_series(self, series_symbol: str) -> list[str]:
|
493
|
+
"""
|
494
|
+
List all futures in a given series.
|
338
495
|
|
339
496
|
Args:
|
340
|
-
series_symbol: the
|
341
|
-
e.g. ES CME Futures" would yield a list of all the ES futures
|
497
|
+
series_symbol: the futures series
|
498
|
+
e.g. "ES CME Futures" would yield a list of all the ES futures
|
342
499
|
Returns:
|
343
|
-
|
500
|
+
List of futures products
|
344
501
|
"""
|
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
|
502
|
+
if not series_symbol.endswith("Futures"):
|
503
|
+
raise ValueError("series_symbol must end with 'Futures'")
|
504
|
+
res = await self.graphql_client.get_future_series_query(series_symbol)
|
505
|
+
return res.futures_series
|
353
506
|
|
354
507
|
@staticmethod
|
355
|
-
def get_expiration_from_CME_name(name: str) -> date:
|
508
|
+
def get_expiration_from_CME_name(name: str) -> Optional[date]:
|
356
509
|
"""
|
510
|
+
@deprecated(reason="Use utils.symbol_parsing.nominative_expiration instead")
|
511
|
+
|
357
512
|
Get the expiration date from a CME future name.
|
358
513
|
|
359
514
|
Args:
|
@@ -362,12 +517,13 @@ class AsyncClient:
|
|
362
517
|
Returns:
|
363
518
|
the expiration date as a date object
|
364
519
|
"""
|
365
|
-
|
366
|
-
return datetime.strptime(d, "%Y%m%d").date()
|
520
|
+
return nominative_expiration(name)
|
367
521
|
|
368
522
|
async def get_cme_futures_series(self, series: str) -> list[tuple[date, str]]:
|
369
523
|
"""
|
370
|
-
|
524
|
+
@deprecated(reason="Use get_futures_series instead")
|
525
|
+
|
526
|
+
List all futures in a given CME series.
|
371
527
|
|
372
528
|
Args:
|
373
529
|
series: the series to get the futures for
|
@@ -377,39 +533,37 @@ class AsyncClient:
|
|
377
533
|
the symbol for each future in the series
|
378
534
|
|
379
535
|
e.g.
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
536
|
+
```
|
537
|
+
[
|
538
|
+
(datetime.date(2025, 3, 21), 'ES 20250321 CME Future'),
|
539
|
+
(datetime.date(2025, 6, 20), 'ES 20250620 CME Future'),
|
540
|
+
(datetime.date(2025, 9, 19), 'ES 20250919 CME Future'),
|
541
|
+
# ...
|
384
542
|
]
|
543
|
+
```
|
385
544
|
"""
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
]
|
393
|
-
|
394
|
-
filtered_markets.sort(key=lambda x: x[0])
|
395
|
-
|
396
|
-
return filtered_markets
|
545
|
+
futures = await self.get_futures_series(series)
|
546
|
+
exp_and_futures = []
|
547
|
+
for future in futures:
|
548
|
+
exp = nominative_expiration(future)
|
549
|
+
if exp is not None:
|
550
|
+
exp_and_futures.append((exp, future))
|
551
|
+
exp_and_futures.sort(key=lambda x: x[0])
|
552
|
+
return exp_and_futures
|
397
553
|
|
398
554
|
async def get_cme_future_from_root_month_year(
|
399
555
|
self, root: str, month: int, year: int
|
400
556
|
) -> str:
|
401
557
|
"""
|
402
558
|
Get the symbol for a CME future from the root, month, and year.
|
559
|
+
This is a simple wrapper around search_symbols.
|
403
560
|
|
404
561
|
Args:
|
405
562
|
root: the root symbol for the future e.g. "ES"
|
406
563
|
month: the month of the future
|
407
564
|
year: the year of the future
|
408
565
|
Returns:
|
409
|
-
|
410
|
-
|
411
|
-
Errors if the result is not unique
|
412
|
-
This is a simple wrapper around search_symbols
|
566
|
+
The future symbol if it exists and is unique.
|
413
567
|
"""
|
414
568
|
[market] = [
|
415
569
|
market
|
@@ -419,101 +573,507 @@ class AsyncClient:
|
|
419
573
|
)
|
420
574
|
if market.startswith(f"{root} {year}{month:02d}")
|
421
575
|
]
|
422
|
-
|
423
576
|
return market
|
424
577
|
|
425
578
|
# ------------------------------------------------------------
|
426
|
-
#
|
579
|
+
# Marketdata
|
427
580
|
# ------------------------------------------------------------
|
428
581
|
|
429
|
-
async def
|
582
|
+
async def get_market_status(
|
583
|
+
self, symbol: TradableProduct | str, venue: Venue
|
584
|
+
) -> MarketStatus:
|
430
585
|
"""
|
431
|
-
|
586
|
+
Returns market status for symbol (e.g. if it's currently quoting or trading).
|
587
|
+
|
588
|
+
Args:
|
589
|
+
symbol: the symbol to get the market status for, e.g. "ES 20250321 CME Future/USD"
|
590
|
+
venue: the venue that the symbol is traded at, e.g. CME
|
591
|
+
"""
|
592
|
+
grpc_client = await self.marketdata(venue)
|
593
|
+
req = MarketStatusRequest(symbol=str(symbol), venue=venue)
|
594
|
+
res: MarketStatus = await grpc_client.unary_unary(req)
|
595
|
+
return res
|
596
|
+
|
597
|
+
async def get_market_snapshot(
|
598
|
+
self, symbol: TradableProduct | str, venue: Venue
|
599
|
+
) -> L1BookSnapshot:
|
600
|
+
"""
|
601
|
+
@deprecated(reason="Use get_l1_snapshot instead")
|
432
602
|
|
603
|
+
This is an alias for l1_book_snapshot.
|
604
|
+
|
605
|
+
Args:
|
606
|
+
symbol: the symbol to get the market snapshot for, e.g. "ES 20250321 CME Future/USD"
|
607
|
+
venue: the venue that the symbol is traded at, e.g. CME
|
433
608
|
Returns:
|
434
|
-
|
609
|
+
L1BookSnapshot for the symbol
|
610
|
+
"""
|
611
|
+
return await self.get_l1_book_snapshot(symbol=symbol, venue=venue)
|
612
|
+
|
613
|
+
async def get_market_snapshots(
|
614
|
+
self, symbols: list[TradableProduct | str], venue: Venue
|
615
|
+
) -> Sequence[L1BookSnapshot]:
|
616
|
+
"""
|
617
|
+
@deprecated(reason="Use get_l1_snapshots instead")
|
618
|
+
|
619
|
+
This is an alias for l1_book_snapshots.
|
620
|
+
|
621
|
+
Args:
|
622
|
+
symbols: the symbols to get the market snapshots for
|
623
|
+
venue: the venue that the symbols are traded at
|
624
|
+
"""
|
625
|
+
return await self.get_l1_book_snapshots(
|
626
|
+
venue=venue,
|
627
|
+
symbols=symbols, # pyright: ignore[reportArgumentType]
|
628
|
+
)
|
629
|
+
|
630
|
+
@overload
|
631
|
+
async def get_historical_candles(
|
632
|
+
self,
|
633
|
+
symbol: TradableProduct | str,
|
634
|
+
venue: Venue,
|
635
|
+
candle_width: CandleWidth,
|
636
|
+
start: datetime,
|
637
|
+
end: datetime,
|
638
|
+
*,
|
639
|
+
as_dataframe: Literal[True],
|
640
|
+
) -> pd.DataFrame: ...
|
641
|
+
|
642
|
+
@overload
|
643
|
+
async def get_historical_candles(
|
644
|
+
self,
|
645
|
+
symbol: TradableProduct | str,
|
646
|
+
venue: Venue,
|
647
|
+
candle_width: CandleWidth,
|
648
|
+
start: datetime,
|
649
|
+
end: datetime,
|
650
|
+
) -> List[Candle]: ...
|
651
|
+
|
652
|
+
async def get_historical_candles(
|
653
|
+
self,
|
654
|
+
symbol: TradableProduct | str,
|
655
|
+
venue: Venue,
|
656
|
+
candle_width: CandleWidth,
|
657
|
+
start: datetime,
|
658
|
+
end: datetime,
|
659
|
+
*,
|
660
|
+
as_dataframe: bool = False,
|
661
|
+
) -> Union[List[Candle], pd.DataFrame]:
|
662
|
+
"""
|
663
|
+
Gets historical candles for a symbol.
|
664
|
+
|
665
|
+
Args:
|
666
|
+
symbol: the symbol to get the candles for
|
667
|
+
venue: the venue of the symbol
|
668
|
+
candle_width: the width of the candles
|
669
|
+
start: the start date to get candles for;
|
670
|
+
end: the end date to get candles for;
|
671
|
+
as_dataframe: if True, return a pandas DataFrame
|
672
|
+
|
673
|
+
"""
|
674
|
+
grpc_client = await self.hmart()
|
675
|
+
if start.tzinfo is not timezone.utc:
|
676
|
+
raise ValueError(
|
677
|
+
"start must be a utc datetime:\n"
|
678
|
+
"for example datetime.now(timezone.utc) or \n"
|
679
|
+
"dt = datetime(2025, 4, 15, 12, 0, 0, tzinfo=timezone.utc)"
|
680
|
+
)
|
681
|
+
if end.tzinfo is not timezone.utc:
|
682
|
+
raise ValueError(
|
683
|
+
"end must be a utc datetime:\n"
|
684
|
+
"for example datetime.now(timezone.utc) or \n"
|
685
|
+
"dt = datetime(2025, 4, 15, 12, 0, 0, tzinfo=timezone.utc)"
|
686
|
+
)
|
687
|
+
req = HistoricalCandlesRequest(
|
688
|
+
venue=venue,
|
689
|
+
symbol=str(symbol),
|
690
|
+
candle_width=candle_width,
|
691
|
+
start_date=start,
|
692
|
+
end_date=end,
|
693
|
+
)
|
694
|
+
res: HistoricalCandlesResponse = await grpc_client.unary_unary(req)
|
695
|
+
|
696
|
+
if as_dataframe:
|
697
|
+
return candles_to_dataframe(res.candles)
|
698
|
+
else:
|
699
|
+
return res.candles
|
700
|
+
|
701
|
+
async def get_l1_book_snapshot(
|
702
|
+
self,
|
703
|
+
symbol: TradableProduct | str,
|
704
|
+
venue: Venue,
|
705
|
+
) -> L1BookSnapshot:
|
706
|
+
"""
|
707
|
+
Gets the L1 book snapshot for a symbol.
|
708
|
+
|
709
|
+
Args:
|
710
|
+
symbol: the symbol to get the l1 book snapshot for
|
711
|
+
venue: the venue that the symbol is traded at
|
712
|
+
"""
|
713
|
+
grpc_client = await self.marketdata(venue)
|
714
|
+
req = L1BookSnapshotRequest(symbol=str(symbol), venue=venue)
|
715
|
+
res: L1BookSnapshot = await grpc_client.unary_unary(req)
|
716
|
+
return res
|
717
|
+
|
718
|
+
async def get_l1_book_snapshots(
|
719
|
+
self, symbols: list[TradableProduct | str], venue: Venue
|
720
|
+
) -> Sequence[L1BookSnapshot]:
|
721
|
+
"""
|
722
|
+
Gets the L1 book snapshots for a list of symbols.
|
723
|
+
|
724
|
+
Args:
|
725
|
+
symbols: the symbols to get the l1 book snapshots for
|
726
|
+
venue: the venue that the symbols are traded at
|
727
|
+
"""
|
728
|
+
grpc_client = await self.marketdata(venue)
|
729
|
+
req = L1BookSnapshotsRequest(symbols=symbols)
|
730
|
+
res: ArrayOfL1BookSnapshot = await grpc_client.unary_unary(
|
731
|
+
req # pyright: ignore[reportArgumentType]
|
732
|
+
)
|
733
|
+
return res
|
734
|
+
|
735
|
+
async def get_l2_book_snapshot(
|
736
|
+
self, symbol: TradableProduct | str, venue: Venue
|
737
|
+
) -> L2BookSnapshot:
|
738
|
+
"""
|
739
|
+
Gets the L2 book snapshot for a symbol.
|
740
|
+
|
741
|
+
Args:
|
742
|
+
symbol: the symbol to get the l2 book snapshot for
|
743
|
+
venue: the venue that the symbol is traded at
|
744
|
+
"""
|
745
|
+
grpc_client = await self.marketdata(venue)
|
746
|
+
req = L2BookSnapshotRequest(symbol=str(symbol), venue=venue)
|
747
|
+
res: L2BookSnapshot = await grpc_client.unary_unary(req)
|
748
|
+
return res
|
749
|
+
|
750
|
+
async def get_ticker(self, symbol: TradableProduct | str, venue: Venue) -> Ticker:
|
751
|
+
"""
|
752
|
+
Gets the ticker for a symbol.
|
753
|
+
"""
|
754
|
+
grpc_client = await self.marketdata(venue)
|
755
|
+
req = TickerRequest(symbol=str(symbol), venue=venue)
|
756
|
+
res: Ticker = await grpc_client.unary_unary(req)
|
757
|
+
return res
|
758
|
+
|
759
|
+
async def stream_l1_book_snapshots(
|
760
|
+
self, symbols: Sequence[TradableProduct | str], venue: Venue
|
761
|
+
) -> AsyncGenerator[L1BookSnapshot, None]:
|
762
|
+
"""
|
763
|
+
Subscribe to the stream of L1BookSnapshots for a symbol.
|
764
|
+
|
765
|
+
Args:
|
766
|
+
symbols: the symbols to subscribe to;
|
767
|
+
If symbols=None, subscribe to all symbols available for the venue.
|
768
|
+
venue: the venue to subscribe to
|
769
|
+
"""
|
770
|
+
grpc_client = await self.marketdata(venue)
|
771
|
+
req = SubscribeL1BookSnapshotsRequest(symbols=list(symbols), venue=venue)
|
772
|
+
async for res in grpc_client.unary_stream(req):
|
773
|
+
yield res
|
774
|
+
|
775
|
+
async def stream_l2_book_updates(
|
776
|
+
self, symbol: TradableProduct | str, venue: Venue
|
777
|
+
) -> AsyncGenerator[L2BookUpdate, None]:
|
778
|
+
"""
|
779
|
+
Subscribe to the stream of L2BookUpdates for a symbol.
|
780
|
+
|
781
|
+
This stream is a diff stream; to construct and maintain the actual state of
|
782
|
+
the L2 book, apply the updates stream using the method described.
|
783
|
+
|
784
|
+
Args:
|
785
|
+
symbol: the symbol to subscribe to
|
786
|
+
venue: the marketdata venue
|
787
|
+
"""
|
788
|
+
grpc_client = await self.marketdata(venue)
|
789
|
+
req = SubscribeL2BookUpdatesRequest(symbol=str(symbol), venue=venue)
|
790
|
+
async for res in grpc_client.unary_stream(
|
791
|
+
req # pyright: ignore[reportArgumentType]
|
792
|
+
):
|
793
|
+
yield res
|
794
|
+
|
795
|
+
async def subscribe_l1_book(
|
796
|
+
self, symbol: TradableProduct | str, venue: Venue
|
797
|
+
) -> L1BookSnapshot:
|
798
|
+
"""
|
799
|
+
Subscribe to the L1 stream for a symbol in the background.
|
800
|
+
|
801
|
+
If a subscription is already active, the existing reference will be
|
802
|
+
returned; otherwise, a new subscription will be created.
|
803
|
+
|
804
|
+
Snapshots will have an initial value of timestamp=0 and bid/ask=None.
|
805
|
+
|
806
|
+
Args:
|
807
|
+
symbol: the symbol to subscribe to
|
808
|
+
venue: the marketdata venue
|
809
|
+
|
810
|
+
Return:
|
811
|
+
An L1 book object that is constantly updating in the background.
|
812
|
+
"""
|
813
|
+
symbol = TradableProduct(symbol)
|
814
|
+
|
815
|
+
if venue in self.l1_books:
|
816
|
+
if symbol in self.l1_books[venue]:
|
817
|
+
return self.l1_books[venue][symbol][0]
|
818
|
+
else:
|
819
|
+
self.l1_books[venue] = {}
|
820
|
+
|
821
|
+
grpc_client = await self.marketdata(venue)
|
822
|
+
book = L1BookSnapshot(symbol, 0, 0)
|
823
|
+
self.l1_books[venue][symbol] = (
|
824
|
+
book,
|
825
|
+
asyncio.create_task(
|
826
|
+
self.__subscribe_l1_book_task(symbol, venue, grpc_client, book)
|
827
|
+
),
|
828
|
+
)
|
829
|
+
return book
|
830
|
+
|
831
|
+
async def unsubscribe_l1_book(self, symbol: TradableProduct | str, venue: Venue):
|
832
|
+
"""
|
833
|
+
Unsubscribe from the L1 stream for a symbol, ie undoes subscribe_l1_book.
|
834
|
+
"""
|
835
|
+
symbol = TradableProduct(symbol)
|
836
|
+
try:
|
837
|
+
task = self.l1_books[venue][symbol][1]
|
838
|
+
task.cancel()
|
839
|
+
except Exception as e:
|
840
|
+
logging.error(
|
841
|
+
f"Error unsubscribing from L1 book for {symbol}, venue {venue}: {e}"
|
842
|
+
)
|
843
|
+
finally:
|
844
|
+
if venue in self.l1_books and symbol in self.l1_books[venue]:
|
845
|
+
del self.l1_books[venue][symbol]
|
846
|
+
|
847
|
+
async def __subscribe_l1_book_task(
|
848
|
+
self,
|
849
|
+
symbol: TradableProduct,
|
850
|
+
venue: Venue,
|
851
|
+
grpc_client: GrpcClient,
|
852
|
+
book: L1BookSnapshot,
|
853
|
+
):
|
854
|
+
try:
|
855
|
+
req = SubscribeL1BookSnapshotsRequest(symbols=[symbol], venue=venue)
|
856
|
+
stream = grpc_client.unary_stream(req)
|
857
|
+
async for snap in stream:
|
858
|
+
book.tn = snap.tn
|
859
|
+
book.ts = snap.ts
|
860
|
+
book.a = snap.a
|
861
|
+
book.b = snap.b
|
862
|
+
book.rt = snap.rt
|
863
|
+
book.rtn = snap.rtn
|
864
|
+
except Exception as e:
|
865
|
+
logging.error(
|
866
|
+
f"Error subscribing to L1 book for {symbol}, venue {venue}: {e}"
|
867
|
+
)
|
868
|
+
finally:
|
869
|
+
del self.l1_books[venue][symbol]
|
870
|
+
|
871
|
+
async def subscribe_l2_book(
|
872
|
+
self,
|
873
|
+
symbol: TradableProduct | str,
|
874
|
+
venue: Venue,
|
875
|
+
) -> L2BookSnapshot:
|
876
|
+
"""
|
877
|
+
Subscribe to the L2 stream for a symbol in the background.
|
878
|
+
|
879
|
+
If a subscription is already active, the existing reference will be
|
880
|
+
returned; otherwise, a new subscription will be created.
|
881
|
+
|
882
|
+
Snapshots will have an initial value of timestamp=0 and bids/asks=[].
|
883
|
+
|
884
|
+
Args:
|
885
|
+
symbol: the symbol to subscribe to
|
886
|
+
venue: the marketdata venue
|
887
|
+
|
888
|
+
Return:
|
889
|
+
An L2 book object that is constantly updating in the background.
|
890
|
+
"""
|
891
|
+
symbol = TradableProduct(symbol)
|
892
|
+
|
893
|
+
if venue in self.l2_books:
|
894
|
+
if symbol in self.l2_books[venue]:
|
895
|
+
return self.l2_books[venue][symbol][0]
|
896
|
+
else:
|
897
|
+
self.l2_books[venue] = {}
|
898
|
+
|
899
|
+
grpc_client = await self.marketdata(venue)
|
900
|
+
book = L2BookSnapshot([], [], 0, 0, 0, 0)
|
901
|
+
self.l2_books[venue][symbol] = (
|
902
|
+
book,
|
903
|
+
asyncio.create_task(
|
904
|
+
self.__subscribe_l2_book_task(symbol, venue, grpc_client, book)
|
905
|
+
),
|
906
|
+
)
|
907
|
+
return book
|
908
|
+
|
909
|
+
async def __subscribe_l2_book_task(
|
910
|
+
self,
|
911
|
+
symbol: TradableProduct,
|
912
|
+
venue: Venue,
|
913
|
+
grpc_client: GrpcClient,
|
914
|
+
book: L2BookSnapshot,
|
915
|
+
):
|
916
|
+
try:
|
917
|
+
req = SubscribeL2BookUpdatesRequest(symbol=str(symbol), venue=venue)
|
918
|
+
stream = grpc_client.unary_stream(
|
919
|
+
req # pyright: ignore[reportArgumentType]
|
920
|
+
)
|
921
|
+
async for up in stream:
|
922
|
+
if isinstance(up, L2BookDiff):
|
923
|
+
if (
|
924
|
+
up.sequence_id != book.sequence_id
|
925
|
+
or up.sequence_number != book.sequence_number + 1
|
926
|
+
):
|
927
|
+
raise ValueError(
|
928
|
+
f"Received update out-of-order for L2 book: {symbol}"
|
929
|
+
)
|
930
|
+
book.sid = up.sid
|
931
|
+
book.sn = up.sn
|
932
|
+
book.ts = up.ts
|
933
|
+
book.tn = up.tn
|
934
|
+
for px, sz in up.bids:
|
935
|
+
update_orderbook_side(book.bids, px, sz, ascending=False)
|
936
|
+
for px, sz in up.asks:
|
937
|
+
update_orderbook_side(book.asks, px, sz, ascending=True)
|
938
|
+
elif isinstance(up, L2BookSnapshot):
|
939
|
+
book.sid = up.sid
|
940
|
+
book.sn = up.sn
|
941
|
+
book.ts = up.ts
|
942
|
+
book.tn = up.tn
|
943
|
+
book.a = up.a
|
944
|
+
book.b = up.b
|
945
|
+
except Exception as e:
|
946
|
+
logging.error(
|
947
|
+
f"Error subscribing to L2 book for {symbol}, venue {venue}: {e}"
|
948
|
+
)
|
949
|
+
finally:
|
950
|
+
del self.l2_books[venue][symbol]
|
951
|
+
|
952
|
+
async def stream_trades(
|
953
|
+
self, symbol: TradableProduct | str, venue: Venue
|
954
|
+
) -> AsyncGenerator[Trade, None]:
|
955
|
+
"""
|
956
|
+
Subscribe to a stream of trades for a symbol.
|
957
|
+
"""
|
958
|
+
grpc_client = await self.marketdata(venue)
|
959
|
+
req = SubscribeTradesRequest(symbol=str(symbol), venue=venue)
|
960
|
+
async for res in grpc_client.unary_stream(req):
|
961
|
+
yield res
|
962
|
+
|
963
|
+
async def stream_candles(
|
964
|
+
self,
|
965
|
+
symbol: TradableProduct | str,
|
966
|
+
venue: Venue,
|
967
|
+
candle_widths: Optional[list[CandleWidth]],
|
968
|
+
) -> AsyncGenerator[Candle, None]:
|
969
|
+
"""
|
970
|
+
Subscribe to a stream of candles for a symbol.
|
435
971
|
"""
|
436
|
-
|
972
|
+
grpc_client = await self.marketdata(venue)
|
973
|
+
req = SubscribeCandlesRequest(
|
974
|
+
symbol=str(symbol),
|
975
|
+
venue=venue,
|
976
|
+
candle_widths=candle_widths,
|
977
|
+
)
|
978
|
+
async for res in grpc_client.unary_stream(req):
|
979
|
+
yield res
|
437
980
|
|
438
|
-
|
981
|
+
# ------------------------------------------------------------
|
982
|
+
# Portfolio management
|
983
|
+
# ------------------------------------------------------------
|
439
984
|
|
440
|
-
async def list_accounts(self) ->
|
985
|
+
async def list_accounts(self) -> List[AccountWithPermissions]:
|
441
986
|
"""
|
442
987
|
List accounts for the user that the API key belongs to.
|
443
988
|
|
444
989
|
Returns:
|
445
990
|
a list of AccountWithPermissionsFields for the user that the API key belongs to
|
991
|
+
a list of AccountWithPermissions for the user that the API key belongs to
|
446
992
|
(use who_am_i to get the user_id / email)
|
447
993
|
"""
|
448
|
-
|
449
|
-
|
994
|
+
grpc_client = await self.core()
|
995
|
+
req = AccountsRequest(paper=self.paper_trading)
|
996
|
+
res = await grpc_client.unary_unary(req)
|
997
|
+
return res.accounts
|
450
998
|
|
451
|
-
async def get_account_summary(self, account: str) ->
|
999
|
+
async def get_account_summary(self, account: str) -> AccountSummary:
|
452
1000
|
"""
|
453
|
-
|
1001
|
+
Get account summary, including balances, positions, pnls, etc.
|
454
1002
|
|
455
1003
|
Args:
|
456
|
-
account:
|
457
|
-
|
458
|
-
Returns:
|
459
|
-
AccountSummaryFields for the account
|
1004
|
+
account: account uuid or name
|
1005
|
+
Examples: "00000000-0000-0000-0000-000000000000", "STONEX:000000/JDoe"
|
460
1006
|
"""
|
461
|
-
|
462
|
-
|
1007
|
+
grpc_client = await self.core()
|
1008
|
+
req = AccountSummaryRequest(account=account)
|
1009
|
+
res = await grpc_client.unary_unary(req)
|
1010
|
+
return res
|
463
1011
|
|
464
1012
|
async def get_account_summaries(
|
465
1013
|
self,
|
466
1014
|
accounts: Optional[list[str]] = None,
|
467
1015
|
trader: Optional[str] = None,
|
468
|
-
) ->
|
1016
|
+
) -> list[AccountSummary]:
|
469
1017
|
"""
|
470
|
-
|
1018
|
+
Get account summaries for accounts matching the filters.
|
471
1019
|
|
472
1020
|
Args:
|
473
|
-
accounts:
|
474
|
-
|
475
|
-
trader: the trader / userId to get summaries for
|
1021
|
+
accounts: list of account uuids or names
|
1022
|
+
trader: if specified, return summaries for all accounts for this trader
|
476
1023
|
|
477
|
-
|
478
|
-
Returns:
|
479
|
-
a list of AccountSummary for the accounts
|
1024
|
+
If both arguments are given, the union of matching accounts are returned.
|
480
1025
|
"""
|
481
|
-
|
482
|
-
|
1026
|
+
grpc_client = await self.core()
|
1027
|
+
request = AccountSummariesRequest(
|
1028
|
+
accounts=accounts,
|
1029
|
+
trader=trader,
|
483
1030
|
)
|
484
|
-
|
1031
|
+
res = await grpc_client.unary_unary(request)
|
1032
|
+
return res.account_summaries
|
485
1033
|
|
486
1034
|
async def get_account_history(
|
487
1035
|
self,
|
488
1036
|
account: str,
|
489
1037
|
from_inclusive: Optional[datetime] = None,
|
490
1038
|
to_exclusive: Optional[datetime] = None,
|
491
|
-
) ->
|
1039
|
+
) -> list[AccountSummary]:
|
492
1040
|
"""
|
493
|
-
|
494
|
-
|
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
|
1041
|
+
Get historical sequence of account summaries for the given account.
|
498
1042
|
"""
|
499
|
-
|
1043
|
+
grpc_client = await self.core()
|
1044
|
+
if from_inclusive is not None:
|
1045
|
+
assert from_inclusive.tzinfo is timezone.utc, (
|
1046
|
+
"from_inclusive must be a utc datetime:\n"
|
1047
|
+
"for example datetime.now(timezone.utc) or \n"
|
1048
|
+
"dt = datetime(2025, 4, 15, 12, 0, 0, tzinfo=timezone.utc)"
|
1049
|
+
)
|
1050
|
+
|
1051
|
+
if to_exclusive is not None:
|
1052
|
+
assert to_exclusive.tzinfo is timezone.utc, (
|
1053
|
+
"to_exclusive must be a utc datetime:\n"
|
1054
|
+
"for example datetime.now(timezone.utc) or \n"
|
1055
|
+
"dt = datetime(2025, 4, 15, 12, 0, 0, tzinfo=timezone.utc)"
|
1056
|
+
)
|
1057
|
+
|
1058
|
+
req = AccountHistoryRequest(
|
500
1059
|
account=account, from_inclusive=from_inclusive, to_exclusive=to_exclusive
|
501
1060
|
)
|
502
|
-
|
1061
|
+
res = await grpc_client.unary_unary(req)
|
1062
|
+
return res.history
|
503
1063
|
|
504
1064
|
# ------------------------------------------------------------
|
505
|
-
# Order
|
1065
|
+
# Order management
|
506
1066
|
# ------------------------------------------------------------
|
507
1067
|
|
508
1068
|
async def get_open_orders(
|
509
1069
|
self,
|
510
|
-
order_ids: Optional[list[
|
1070
|
+
order_ids: Optional[list[OrderId]] = None,
|
511
1071
|
venue: Optional[str] = None,
|
512
1072
|
account: Optional[str] = None,
|
513
1073
|
trader: Optional[str] = None,
|
514
1074
|
symbol: Optional[str] = None,
|
515
|
-
parent_order_id: Optional[
|
516
|
-
) ->
|
1075
|
+
parent_order_id: Optional[OrderId] = None,
|
1076
|
+
) -> list[Order]:
|
517
1077
|
"""
|
518
1078
|
Returns a list of open orders for the user that match the filters.
|
519
1079
|
|
@@ -525,12 +1085,11 @@ class AsyncClient:
|
|
525
1085
|
symbol: the symbol to get orders for
|
526
1086
|
parent_order_id: the parent order id to get orders for
|
527
1087
|
|
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
1088
|
Returns:
|
531
|
-
|
1089
|
+
Open orders that match the union of the filters
|
532
1090
|
"""
|
533
|
-
|
1091
|
+
grpc_client = await self.core()
|
1092
|
+
open_orders_request = OpenOrdersRequest(
|
534
1093
|
venue=venue,
|
535
1094
|
account=account,
|
536
1095
|
trader=trader,
|
@@ -538,46 +1097,63 @@ class AsyncClient:
|
|
538
1097
|
parent_order_id=parent_order_id,
|
539
1098
|
order_ids=order_ids,
|
540
1099
|
)
|
541
|
-
return orders.open_orders
|
542
1100
|
|
543
|
-
|
1101
|
+
open_orders = await grpc_client.unary_unary(open_orders_request)
|
1102
|
+
return open_orders.open_orders
|
1103
|
+
|
1104
|
+
async def get_all_open_orders(self) -> list[Order]:
|
544
1105
|
"""
|
545
|
-
|
1106
|
+
@deprecated(reason="Use get_open_orders with no parameters instead")
|
546
1107
|
|
547
|
-
Returns
|
548
|
-
a list of OrderFields of all the open orders for the user
|
1108
|
+
Returns a list of all open orders for the authenticated user.
|
549
1109
|
"""
|
550
|
-
|
551
|
-
return orders.open_orders
|
1110
|
+
return await self.get_open_orders()
|
552
1111
|
|
553
1112
|
async def get_historical_orders(
|
554
1113
|
self,
|
555
|
-
order_ids: Optional[list[
|
1114
|
+
order_ids: Optional[list[OrderId]] = None,
|
556
1115
|
from_inclusive: Optional[datetime] = None,
|
557
1116
|
to_exclusive: Optional[datetime] = None,
|
558
1117
|
venue: Optional[str] = None,
|
559
1118
|
account: Optional[str] = None,
|
560
|
-
parent_order_id: Optional[
|
561
|
-
) ->
|
1119
|
+
parent_order_id: Optional[OrderId] = None,
|
1120
|
+
) -> list[Order]:
|
562
1121
|
"""
|
563
|
-
|
1122
|
+
Returns a list of all historical orders that match the filters.
|
1123
|
+
|
1124
|
+
Historical orders are orders that are not open, having been filled,
|
1125
|
+
canceled, expired, or outed.
|
564
1126
|
|
565
1127
|
Args:
|
566
1128
|
order_ids: a list of order ids to get
|
567
1129
|
from_inclusive: the start date to get orders for
|
568
1130
|
to_exclusive: the end date to get orders for
|
569
1131
|
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)
|
1132
|
+
account: account uuid or name
|
572
1133
|
parent_order_id: the parent order id to get orders for
|
573
1134
|
Returns:
|
574
|
-
|
1135
|
+
Historical orders that match the union of the filters.
|
575
1136
|
|
576
|
-
|
577
|
-
|
578
|
-
the from_inclusive and to_exclusive parameters need to be filled
|
1137
|
+
If order_ids is not specified, then from_inclusive and to_exclusive
|
1138
|
+
MUST be specified.
|
579
1139
|
"""
|
580
|
-
|
1140
|
+
grpc_client = await self.core()
|
1141
|
+
|
1142
|
+
if from_inclusive is not None:
|
1143
|
+
assert from_inclusive.tzinfo is timezone.utc, (
|
1144
|
+
"from_inclusive must be a utc datetime:\n"
|
1145
|
+
"for example datetime.now(timezone.utc) or \n"
|
1146
|
+
"dt = datetime(2025, 4, 15, 12, 0, 0, tzinfo=timezone.utc)"
|
1147
|
+
)
|
1148
|
+
|
1149
|
+
if to_exclusive is not None:
|
1150
|
+
assert to_exclusive.tzinfo is timezone.utc, (
|
1151
|
+
"to_exclusive must be a utc datetime:\n"
|
1152
|
+
"for example datetime.now(timezone.utc) or \n"
|
1153
|
+
"dt = datetime(2025, 4, 15, 12, 0, 0, tzinfo=timezone.utc)"
|
1154
|
+
)
|
1155
|
+
|
1156
|
+
historical_orders_request = HistoricalOrdersRequest.new(
|
581
1157
|
order_ids=order_ids,
|
582
1158
|
venue=venue,
|
583
1159
|
account=account,
|
@@ -585,406 +1161,221 @@ class AsyncClient:
|
|
585
1161
|
from_inclusive=from_inclusive,
|
586
1162
|
to_exclusive=to_exclusive,
|
587
1163
|
)
|
588
|
-
|
1164
|
+
orders = await grpc_client.unary_unary(historical_orders_request)
|
1165
|
+
return orders.orders
|
589
1166
|
|
590
|
-
async def get_order(self, order_id:
|
1167
|
+
async def get_order(self, order_id: OrderId) -> Optional[Order]:
|
591
1168
|
"""
|
592
|
-
Returns the
|
593
|
-
|
1169
|
+
Returns the specified order. Useful for looking at past sent orders.
|
1170
|
+
Queries open_orders first, then queries historical_orders.
|
594
1171
|
|
595
1172
|
Args:
|
596
1173
|
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
1174
|
"""
|
602
|
-
|
603
|
-
|
1175
|
+
grpc_client = await self.core()
|
1176
|
+
req = OpenOrdersRequest.new(
|
1177
|
+
order_ids=[order_id],
|
604
1178
|
)
|
605
|
-
|
606
|
-
for open_order in
|
1179
|
+
res = await grpc_client.unary_unary(req)
|
1180
|
+
for open_order in res.open_orders:
|
607
1181
|
if open_order.id == order_id:
|
608
1182
|
return open_order
|
609
1183
|
|
610
|
-
|
611
|
-
order_ids=[order_id]
|
1184
|
+
req = HistoricalOrdersRequest.new(
|
1185
|
+
order_ids=[order_id],
|
612
1186
|
)
|
1187
|
+
res = await grpc_client.unary_unary(req)
|
1188
|
+
if res.orders and len(res.orders) == 1:
|
1189
|
+
return res.orders[0]
|
613
1190
|
|
614
|
-
|
615
|
-
return historical_orders.historical_orders[0]
|
616
|
-
|
617
|
-
async def get_orders(self, order_ids: list[str]) -> list[Optional[OrderFields]]:
|
1191
|
+
async def get_orders(self, order_ids: list[OrderId]) -> list[Optional[Order]]:
|
618
1192
|
"""
|
619
|
-
Returns
|
620
|
-
|
1193
|
+
Returns the specified orders. Useful for looking at past sent orders.
|
1194
|
+
Plural form of get_order.
|
621
1195
|
|
622
1196
|
Args:
|
623
1197
|
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
1198
|
"""
|
629
|
-
|
1199
|
+
grpc_client = await self.core()
|
1200
|
+
orders_dict: dict[OrderId, Optional[Order]] = {
|
630
1201
|
order_id: None for order_id in order_ids
|
631
1202
|
}
|
1203
|
+
req = OpenOrdersRequest.new(
|
1204
|
+
order_ids=order_ids,
|
1205
|
+
)
|
632
1206
|
|
633
|
-
|
634
|
-
|
635
|
-
).open_orders
|
636
|
-
for open_order in open_orders:
|
1207
|
+
res = await grpc_client.unary_unary(req)
|
1208
|
+
for open_order in res.open_orders:
|
637
1209
|
orders_dict[open_order.id] = open_order
|
638
1210
|
|
639
1211
|
not_open_order_ids = [
|
640
1212
|
order_id for order_id in order_ids if orders_dict[order_id] is None
|
641
1213
|
]
|
642
1214
|
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
for historical_order in historical_orders:
|
1215
|
+
req = HistoricalOrdersRequest.new(
|
1216
|
+
order_ids=not_open_order_ids,
|
1217
|
+
)
|
1218
|
+
res = await grpc_client.unary_unary(req)
|
1219
|
+
for historical_order in res.orders:
|
649
1220
|
orders_dict[historical_order.id] = historical_order
|
650
1221
|
|
651
1222
|
return [orders_dict[order_id] for order_id in order_ids]
|
652
1223
|
|
653
1224
|
async def get_fills(
|
654
1225
|
self,
|
655
|
-
from_inclusive: Optional[datetime],
|
656
|
-
to_exclusive: Optional[datetime],
|
1226
|
+
from_inclusive: Optional[datetime] = None,
|
1227
|
+
to_exclusive: Optional[datetime] = None,
|
657
1228
|
venue: Optional[str] = None,
|
658
1229
|
account: Optional[str] = None,
|
659
|
-
order_id: Optional[
|
660
|
-
|
1230
|
+
order_id: Optional[OrderId] = None,
|
1231
|
+
limit: Optional[int] = None,
|
1232
|
+
) -> HistoricalFillsResponse:
|
661
1233
|
"""
|
662
|
-
Returns
|
1234
|
+
Returns all fills matching the given filters.
|
663
1235
|
|
664
1236
|
Args:
|
665
1237
|
from_inclusive: the start date to get fills for
|
666
1238
|
to_exclusive: the end date to get fills for
|
667
1239
|
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)
|
1240
|
+
account: account uuid or name
|
670
1241
|
order_id: the order id to get fills for
|
671
|
-
Returns:
|
672
|
-
a list of GetFillsQueryFolioHistoricalFills
|
673
|
-
"""
|
674
|
-
fills = await self.graphql_client.get_fills_query(
|
675
|
-
venue, account, order_id, from_inclusive, to_exclusive
|
676
|
-
)
|
677
|
-
return fills.historical_fills
|
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).
|
688
|
-
|
689
|
-
Args:
|
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(
|
729
|
-
self,
|
730
|
-
symbol: str,
|
731
|
-
candle_width: grpc_definitions.CandleWidth,
|
732
|
-
start: datetime,
|
733
|
-
end: datetime,
|
734
|
-
) -> HistoricalCandlesResponse:
|
735
1242
|
"""
|
736
|
-
|
1243
|
+
grpc_client = await self.core()
|
1244
|
+
if from_inclusive is not None:
|
1245
|
+
assert from_inclusive.tzinfo is timezone.utc, (
|
1246
|
+
"from_inclusive must be a utc datetime:\n"
|
1247
|
+
"for example datetime.now(timezone.utc) or \n"
|
1248
|
+
"dt = datetime(2025, 4, 15, 12, 0, 0, tzinfo=timezone.utc)"
|
1249
|
+
)
|
737
1250
|
|
738
|
-
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
end_date=end,
|
1251
|
+
if to_exclusive is not None:
|
1252
|
+
assert to_exclusive.tzinfo is timezone.utc, (
|
1253
|
+
"to_exclusive must be a utc datetime:\n"
|
1254
|
+
"for example datetime.now(timezone.utc) or \n"
|
1255
|
+
"dt = datetime(2025, 4, 15, 12, 0, 0, tzinfo=timezone.utc)"
|
1256
|
+
)
|
1257
|
+
req = HistoricalFillsRequest(
|
1258
|
+
account=account,
|
1259
|
+
from_inclusive=from_inclusive,
|
1260
|
+
limit=limit,
|
1261
|
+
order_id=order_id,
|
1262
|
+
to_exclusive=to_exclusive,
|
1263
|
+
venue=venue,
|
752
1264
|
)
|
1265
|
+
res = await grpc_client.unary_unary(req)
|
1266
|
+
return res
|
753
1267
|
|
754
|
-
|
755
|
-
|
756
|
-
async def get_l1_book_snapshot(
|
1268
|
+
async def orderflow(
|
757
1269
|
self,
|
758
|
-
|
759
|
-
|
760
|
-
) -> MarketTickerFields:
|
1270
|
+
request_iterator: AsyncIterator[OrderflowRequest],
|
1271
|
+
) -> AsyncGenerator[Orderflow, None]:
|
761
1272
|
"""
|
762
|
-
|
1273
|
+
A two-way channel for both order entry and listening to order updates (fills, acks, outs, etc.).
|
763
1274
|
|
764
|
-
|
765
|
-
symbol: the symbol to get the l1 book snapshot for
|
766
|
-
venue: the venue that the symbol is traded at
|
767
|
-
Returns:
|
768
|
-
MarketTickerFields for the symbol
|
769
|
-
"""
|
770
|
-
snapshot = await self.graphql_client.get_l_1_book_snapshot_query(
|
771
|
-
symbol=symbol, venue=venue
|
772
|
-
)
|
773
|
-
return snapshot.ticker
|
1275
|
+
This is considered the most efficient way to trade in this SDK.
|
774
1276
|
|
775
|
-
|
776
|
-
|
777
|
-
) -> Sequence[MarketTickerFields]:
|
778
|
-
"""
|
779
|
-
Gets the L1 book snapshots for a list of symbols.
|
1277
|
+
Example:
|
1278
|
+
See test_orderflow.py for an example.
|
780
1279
|
|
781
|
-
|
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
|
1280
|
+
This WILL block the event loop until the stream is closed.
|
786
1281
|
"""
|
787
|
-
|
788
|
-
|
1282
|
+
grpc_client = await self.core()
|
1283
|
+
decoder = grpc_client.get_decoder(OrderflowRequestUnannotatedResponseType)
|
1284
|
+
stub: grpc.aio.StreamStreamMultiCallable = grpc_client.channel.stream_stream(
|
1285
|
+
OrderflowRequest_route,
|
1286
|
+
request_serializer=grpc_client.encoder().encode,
|
1287
|
+
response_deserializer=decoder.decode,
|
789
1288
|
)
|
790
|
-
|
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
|
1289
|
+
call: grpc.aio._base_call.StreamStreamCall[OrderflowRequest, Orderflow] = stub(
|
1290
|
+
request_iterator, metadata=(("authorization", f"Bearer {grpc_client.jwt}"),)
|
807
1291
|
)
|
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
|
847
|
-
|
848
|
-
async def subscribe_l1_book(
|
849
|
-
self, symbols: list[TradableProduct]
|
850
|
-
) -> list[L1BookSnapshot]:
|
851
|
-
"""
|
852
|
-
Returns a L1BookSnapshot object that is constantly updating in the background.
|
1292
|
+
async for update in call:
|
1293
|
+
yield update
|
853
1294
|
|
854
|
-
|
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(
|
1295
|
+
async def stream_orderflow(
|
885
1296
|
self,
|
886
|
-
|
887
|
-
|
888
|
-
|
1297
|
+
account: Optional[AccountIdOrName] = None,
|
1298
|
+
execution_venue: Optional[str] = None,
|
1299
|
+
trader: Optional[TraderIdOrEmail] = None,
|
1300
|
+
) -> AsyncGenerator[Orderflow, None]:
|
889
1301
|
"""
|
890
|
-
|
891
|
-
|
892
|
-
Args:
|
893
|
-
symbols: the symbols to subscribe to
|
894
|
-
Return:
|
895
|
-
a list of L2BookSnapshot object that is constantly updating in the background
|
896
|
-
For the duration of the program, the client will be subscribed to the stream
|
897
|
-
and be updating the L2BookSnapshot.
|
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
|
1302
|
+
A stream for listening to order updates (fills, acks, outs, etc.).
|
925
1303
|
|
926
|
-
|
927
|
-
|
928
|
-
|
929
|
-
|
930
|
-
|
931
|
-
|
932
|
-
request = SubscribeTradesRequest(symbol=symbol, venue=venue)
|
933
|
-
return self.grpc_client.subscribe(request)
|
1304
|
+
Example:
|
1305
|
+
```python
|
1306
|
+
request = SubscribeOrderflowRequest.new()
|
1307
|
+
async for of in client.subscribe_orderflow_stream(request):
|
1308
|
+
print(of)
|
1309
|
+
```
|
934
1310
|
|
935
|
-
|
936
|
-
self,
|
937
|
-
symbol: TradableProduct,
|
938
|
-
venue: Optional[str],
|
939
|
-
candle_widths: Optional[list[grpc_definitions.CandleWidth]],
|
940
|
-
) -> AsyncIterator[Candle]:
|
941
|
-
"""
|
942
|
-
Subscribe to a stream of candles for a symbol.
|
1311
|
+
This WILL block the event loop until the stream is closed.
|
943
1312
|
"""
|
944
|
-
|
945
|
-
|
946
|
-
|
947
|
-
|
1313
|
+
grpc_client = await self.core()
|
1314
|
+
request: SubscribeOrderflowRequest = SubscribeOrderflowRequest(
|
1315
|
+
account=account, execution_venue=execution_venue, trader=trader
|
1316
|
+
)
|
1317
|
+
grpc_client = await self.core()
|
1318
|
+
decoder = grpc_client.get_decoder(SubscribeOrderflowRequest)
|
1319
|
+
stub: grpc.aio.UnaryStreamMultiCallable[
|
1320
|
+
SubscribeOrderflowRequest, Orderflow
|
1321
|
+
] = grpc_client.channel.unary_stream(
|
1322
|
+
SubscribeOrderflowRequest.get_route(),
|
1323
|
+
request_serializer=grpc_client.encoder().encode,
|
1324
|
+
response_deserializer=decoder.decode,
|
948
1325
|
)
|
949
|
-
|
1326
|
+
call: grpc.aio._base_call.UnaryStreamCall[
|
1327
|
+
SubscribeOrderflowRequest, Orderflow
|
1328
|
+
] = stub(request, metadata=(("authorization", f"Bearer {grpc_client.jwt}"),))
|
1329
|
+
async for update in call:
|
1330
|
+
yield update
|
950
1331
|
|
951
1332
|
# ------------------------------------------------------------
|
952
|
-
# Order
|
1333
|
+
# Order entry
|
953
1334
|
# ------------------------------------------------------------
|
954
1335
|
|
955
1336
|
async def send_limit_order(
|
1337
|
+
self,
|
1338
|
+
*args,
|
1339
|
+
**kwargs,
|
1340
|
+
) -> Order:
|
1341
|
+
"""
|
1342
|
+
@deprecated(reason="Use place_limit_order instead")
|
1343
|
+
"""
|
1344
|
+
return await self.place_limit_order(*args, **kwargs)
|
1345
|
+
|
1346
|
+
async def place_limit_order(
|
956
1347
|
self,
|
957
1348
|
*,
|
958
|
-
id: Optional[
|
959
|
-
symbol: TradableProduct,
|
960
|
-
execution_venue: Optional[str],
|
961
|
-
|
1349
|
+
id: Optional[OrderId] = None,
|
1350
|
+
symbol: TradableProduct | str,
|
1351
|
+
execution_venue: Optional[str] = None,
|
1352
|
+
dir: Optional[OrderDir] = None,
|
962
1353
|
quantity: Decimal,
|
963
1354
|
limit_price: Decimal,
|
964
1355
|
order_type: OrderType = OrderType.LIMIT,
|
965
|
-
time_in_force: TimeInForce =
|
966
|
-
good_til_date: Optional[datetime] = None,
|
1356
|
+
time_in_force: TimeInForce = TimeInForceEnum.DAY,
|
967
1357
|
price_round_method: Optional[TickRoundMethod] = None,
|
968
1358
|
account: Optional[str] = None,
|
969
1359
|
trader: Optional[str] = None,
|
970
1360
|
post_only: bool = False,
|
971
1361
|
trigger_price: Optional[Decimal] = None,
|
972
|
-
|
1362
|
+
**kwargs: Any,
|
1363
|
+
) -> Order:
|
973
1364
|
"""
|
974
1365
|
Sends a regular limit order.
|
975
1366
|
|
976
1367
|
Args:
|
1368
|
+
id: in case user wants to generate their own order id, otherwise it will be generated automatically
|
977
1369
|
symbol: the symbol to send the order for
|
978
1370
|
execution_venue: the execution venue to send the order to,
|
979
1371
|
if execution_venue is set to None, the OMS will send the order to the primary_exchange
|
980
1372
|
the primary_exchange can be deduced from `get_product_info`
|
981
|
-
|
1373
|
+
dir: the direction of the order, BUY or SELL
|
982
1374
|
quantity: the quantity of the order
|
983
1375
|
limit_price: the limit price of the order
|
984
1376
|
It is highly recommended to make this a Decimal object from the decimal module to avoid floating point errors
|
985
1377
|
order_type: the type of the order
|
986
1378
|
time_in_force: the time in force of the order
|
987
|
-
good_til_date: the date the order is good until, only relevant for time_in_force = "GTD"
|
988
1379
|
price_round_method: the method to round the price to the nearest tick, will not round if None
|
989
1380
|
account: the account to send the order for
|
990
1381
|
While technically optional, for most order types, the account is required
|
@@ -993,13 +1384,21 @@ class AsyncClient:
|
|
993
1384
|
post_only: whether the order should be post only, not supported by all exchanges
|
994
1385
|
trigger_price: the trigger price for the order, only relevant for stop / take_profit orders
|
995
1386
|
Returns:
|
996
|
-
the
|
1387
|
+
the Order object for the order
|
997
1388
|
The order.status should be "PENDING" until the order is "OPEN" / "REJECTED" / "OUT" / "CANCELED" / "STALE"
|
998
1389
|
|
999
1390
|
If the order is rejected, the order.reject_reason and order.reject_message will be set
|
1000
1391
|
"""
|
1392
|
+
grpc_client = await self.core()
|
1001
1393
|
assert quantity > 0, "quantity must be positive"
|
1002
1394
|
|
1395
|
+
if dir is None:
|
1396
|
+
if "odir" in kwargs and isinstance(kwargs["odir"], OrderDir):
|
1397
|
+
logging.warning("odir is deprecated, use dir instead")
|
1398
|
+
dir = kwargs["odir"]
|
1399
|
+
else:
|
1400
|
+
raise ValueError("dir is required")
|
1401
|
+
|
1003
1402
|
if price_round_method is not None:
|
1004
1403
|
if execution_venue is None:
|
1005
1404
|
product_info = await self.get_product_info(symbol)
|
@@ -1025,44 +1424,43 @@ class AsyncClient:
|
|
1025
1424
|
else:
|
1026
1425
|
raise ValueError(f"Could not find market information for {symbol}")
|
1027
1426
|
|
1028
|
-
|
1029
|
-
|
1030
|
-
|
1031
|
-
|
1032
|
-
|
1033
|
-
|
1034
|
-
|
1035
|
-
|
1036
|
-
|
1037
|
-
|
1038
|
-
|
1039
|
-
|
1040
|
-
|
1041
|
-
post_only,
|
1042
|
-
trigger_price,
|
1043
|
-
good_til_date,
|
1044
|
-
execution_venue,
|
1427
|
+
req: PlaceOrderRequest = PlaceOrderRequest.new(
|
1428
|
+
dir=dir,
|
1429
|
+
quantity=quantity,
|
1430
|
+
symbol=symbol,
|
1431
|
+
time_in_force=time_in_force,
|
1432
|
+
limit_price=limit_price,
|
1433
|
+
order_type=order_type,
|
1434
|
+
account=account,
|
1435
|
+
id=id,
|
1436
|
+
parent_id=None,
|
1437
|
+
source=OrderSource.API,
|
1438
|
+
trader=trader,
|
1439
|
+
execution_venue=execution_venue,
|
1440
|
+
post_only=post_only,
|
1441
|
+
trigger_price=trigger_price,
|
1045
1442
|
)
|
1046
|
-
|
1047
|
-
return
|
1443
|
+
res = await grpc_client.unary_unary(req)
|
1444
|
+
return res
|
1048
1445
|
|
1049
1446
|
async def send_market_pro_order(
|
1050
1447
|
self,
|
1051
1448
|
*,
|
1052
|
-
id: Optional[
|
1053
|
-
symbol: TradableProduct,
|
1449
|
+
id: Optional[OrderId] = None,
|
1450
|
+
symbol: TradableProduct | str,
|
1054
1451
|
execution_venue: str,
|
1055
1452
|
odir: OrderDir,
|
1056
1453
|
quantity: Decimal,
|
1057
|
-
time_in_force: TimeInForce =
|
1454
|
+
time_in_force: TimeInForce = TimeInForceEnum.DAY,
|
1058
1455
|
account: Optional[str] = None,
|
1059
1456
|
fraction_through_market: Decimal = Decimal("0.001"),
|
1060
|
-
) ->
|
1457
|
+
) -> Order:
|
1061
1458
|
"""
|
1062
1459
|
Sends a market-order like limit price based on the BBO.
|
1063
1460
|
Meant to behave as a market order but with more protections.
|
1064
1461
|
|
1065
1462
|
Args:
|
1463
|
+
id: in case user wants to generate their own order id, otherwise it will be generated automatically
|
1066
1464
|
symbol: the symbol to send the order for
|
1067
1465
|
execution_venue: the execution venue to send the order to
|
1068
1466
|
odir: the direction of the order
|
@@ -1078,36 +1476,33 @@ class AsyncClient:
|
|
1078
1476
|
If the order is rejected, the order.reject_reason and order.reject_message will be set
|
1079
1477
|
"""
|
1080
1478
|
|
1081
|
-
|
1082
|
-
|
1083
|
-
symbol=symbol, venue=execution_venue
|
1084
|
-
)
|
1085
|
-
if bbo_snapshot is None:
|
1479
|
+
ticker = await self.get_ticker(symbol, execution_venue)
|
1480
|
+
if ticker is None:
|
1086
1481
|
raise ValueError(
|
1087
|
-
f"Failed to send market order with reason: no
|
1482
|
+
f"Failed to send market order with reason: no ticker for {symbol}"
|
1088
1483
|
)
|
1089
1484
|
|
1090
1485
|
price_band = price_band_pairs.get(symbol, None)
|
1091
1486
|
|
1092
1487
|
if odir == OrderDir.BUY:
|
1093
|
-
if
|
1488
|
+
if ticker.ask_price is None:
|
1094
1489
|
raise ValueError(
|
1095
1490
|
f"Failed to send market order with reason: no ask price for {symbol}"
|
1096
1491
|
)
|
1097
|
-
limit_price =
|
1492
|
+
limit_price = ticker.ask_price * (1 + fraction_through_market)
|
1098
1493
|
|
1099
|
-
if price_band and
|
1100
|
-
price_band_reference_price =
|
1494
|
+
if price_band and ticker.last_price:
|
1495
|
+
price_band_reference_price = ticker.last_price + price_band
|
1101
1496
|
limit_price = min(limit_price, price_band_reference_price)
|
1102
1497
|
|
1103
1498
|
else:
|
1104
|
-
if
|
1499
|
+
if ticker.bid_price is None:
|
1105
1500
|
raise ValueError(
|
1106
1501
|
f"Failed to send market order with reason: no bid price for {symbol}"
|
1107
1502
|
)
|
1108
|
-
limit_price =
|
1109
|
-
if price_band and
|
1110
|
-
price_band_reference_price =
|
1503
|
+
limit_price = ticker.bid_price * (1 - fraction_through_market)
|
1504
|
+
if price_band and ticker.last_price:
|
1505
|
+
price_band_reference_price = ticker.last_price - price_band
|
1111
1506
|
limit_price = min(limit_price, price_band_reference_price)
|
1112
1507
|
|
1113
1508
|
# Conservatively round price to nearest tick
|
@@ -1125,7 +1520,7 @@ class AsyncClient:
|
|
1125
1520
|
):
|
1126
1521
|
limit_price = tick_round_method(limit_price, tick_size)
|
1127
1522
|
|
1128
|
-
return await self.
|
1523
|
+
return await self.place_limit_order(
|
1129
1524
|
id=id,
|
1130
1525
|
symbol=symbol,
|
1131
1526
|
execution_venue=execution_venue,
|
@@ -1137,7 +1532,7 @@ class AsyncClient:
|
|
1137
1532
|
time_in_force=time_in_force,
|
1138
1533
|
)
|
1139
1534
|
|
1140
|
-
async def cancel_order(self, order_id:
|
1535
|
+
async def cancel_order(self, order_id: OrderId) -> Cancel:
|
1141
1536
|
"""
|
1142
1537
|
Cancels an order by order id.
|
1143
1538
|
|
@@ -1146,10 +1541,17 @@ class AsyncClient:
|
|
1146
1541
|
Returns:
|
1147
1542
|
the CancelFields object
|
1148
1543
|
"""
|
1149
|
-
|
1150
|
-
|
1544
|
+
grpc_client = await self.core()
|
1545
|
+
req = CancelOrderRequest(id=order_id)
|
1546
|
+
res = await grpc_client.unary_unary(req)
|
1547
|
+
return res
|
1151
1548
|
|
1152
|
-
async def cancel_all_orders(
|
1549
|
+
async def cancel_all_orders(
|
1550
|
+
self,
|
1551
|
+
account: Optional[AccountIdOrName] = None,
|
1552
|
+
execution_venue: Optional[str] = None,
|
1553
|
+
trader: Optional[TraderIdOrEmail] = None,
|
1554
|
+
) -> bool:
|
1153
1555
|
"""
|
1154
1556
|
Cancels all open orders.
|
1155
1557
|
|
@@ -1157,5 +1559,25 @@ class AsyncClient:
|
|
1157
1559
|
True if all orders were cancelled successfully
|
1158
1560
|
False if there was an error
|
1159
1561
|
"""
|
1160
|
-
|
1161
|
-
|
1562
|
+
|
1563
|
+
open_orders = await self.get_open_orders(
|
1564
|
+
account=account,
|
1565
|
+
venue=execution_venue,
|
1566
|
+
trader=trader,
|
1567
|
+
)
|
1568
|
+
outputs = await asyncio.gather(
|
1569
|
+
*(self.cancel_order(order.id) for order in open_orders)
|
1570
|
+
)
|
1571
|
+
|
1572
|
+
for cancel in outputs:
|
1573
|
+
if cancel.reject_reason is not None:
|
1574
|
+
return False
|
1575
|
+
return True
|
1576
|
+
grpc_client = await self.core()
|
1577
|
+
req = CancelAllOrdersRequest(
|
1578
|
+
account=account,
|
1579
|
+
execution_venue=execution_venue,
|
1580
|
+
trader=trader,
|
1581
|
+
)
|
1582
|
+
res = await grpc_client.unary_unary(req)
|
1583
|
+
return res
|