architect-py 3.2.1__py3-none-any.whl → 5.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- architect_py/__init__.py +18 -2
- architect_py/async_client.py +1089 -658
- architect_py/client.py +36 -33
- architect_py/client_interface.py +63 -0
- architect_py/common_types/__init__.py +6 -0
- architect_py/common_types/order_dir.py +91 -0
- architect_py/common_types/scalars.py +25 -0
- architect_py/common_types/tradable_product.py +59 -0
- architect_py/graphql_client/__init__.py +2 -0
- architect_py/graphql_client/client.py +3 -6
- architect_py/graphql_client/enums.py +5 -0
- architect_py/graphql_client/fragments.py +3 -6
- architect_py/graphql_client/get_fills_query.py +2 -1
- architect_py/graphql_client/search_symbols_query.py +2 -1
- architect_py/graphql_client/subscribe_orderflow.py +2 -1
- architect_py/graphql_client/subscribe_trades.py +2 -1
- architect_py/grpc/__init__.py +145 -0
- architect_py/grpc/client.py +94 -0
- architect_py/{grpc_client → grpc/models}/Accounts/AccountsRequest.py +6 -3
- architect_py/{grpc_client → grpc/models}/Accounts/AccountsResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Accounts/__init__.py +1 -1
- architect_py/grpc/models/Algo/AlgoOrder.py +114 -0
- architect_py/grpc/models/Algo/AlgoOrderRequest.py +46 -0
- architect_py/grpc/models/Algo/AlgoOrdersRequest.py +72 -0
- architect_py/grpc/models/Algo/AlgoOrdersResponse.py +27 -0
- architect_py/grpc/models/Algo/CreateAlgoOrderRequest.py +56 -0
- architect_py/grpc/models/Algo/PauseAlgoRequest.py +42 -0
- architect_py/grpc/models/Algo/PauseAlgoResponse.py +20 -0
- architect_py/grpc/models/Algo/StartAlgoRequest.py +42 -0
- architect_py/grpc/models/Algo/StartAlgoResponse.py +20 -0
- architect_py/grpc/models/Algo/StopAlgoRequest.py +42 -0
- architect_py/grpc/models/Algo/StopAlgoResponse.py +20 -0
- architect_py/{grpc_client → grpc/models}/Algo/__init__.py +1 -1
- architect_py/grpc/models/Auth/CreateJwtRequest.py +47 -0
- architect_py/grpc/models/Auth/CreateJwtResponse.py +23 -0
- architect_py/{grpc_client/Cpty → grpc/models/Auth}/__init__.py +1 -1
- architect_py/grpc/models/Boss/DepositsRequest.py +40 -0
- architect_py/grpc/models/Boss/DepositsResponse.py +27 -0
- architect_py/grpc/models/Boss/RqdAccountStatisticsRequest.py +42 -0
- architect_py/grpc/models/Boss/RqdAccountStatisticsResponse.py +25 -0
- architect_py/grpc/models/Boss/StatementUrlRequest.py +40 -0
- architect_py/grpc/models/Boss/StatementUrlResponse.py +23 -0
- architect_py/grpc/models/Boss/StatementsRequest.py +40 -0
- architect_py/grpc/models/Boss/StatementsResponse.py +27 -0
- architect_py/grpc/models/Boss/WithdrawalsRequest.py +40 -0
- architect_py/grpc/models/Boss/WithdrawalsResponse.py +27 -0
- architect_py/{grpc_client/Folio → grpc/models/Boss}/__init__.py +1 -1
- architect_py/grpc/models/Core/ConfigRequest.py +37 -0
- architect_py/grpc/models/Core/ConfigResponse.py +25 -0
- architect_py/grpc/models/Core/__init__.py +2 -0
- architect_py/{grpc_client → grpc/models}/Cpty/CptyRequest.py +5 -4
- architect_py/{grpc_client → grpc/models}/Cpty/CptyResponse.py +6 -6
- architect_py/grpc/models/Cpty/CptyStatus.py +48 -0
- architect_py/grpc/models/Cpty/CptyStatusRequest.py +45 -0
- architect_py/grpc/models/Cpty/CptysRequest.py +37 -0
- architect_py/grpc/models/Cpty/CptysResponse.py +27 -0
- architect_py/grpc/models/Cpty/__init__.py +2 -0
- architect_py/{grpc_client → grpc/models}/Folio/AccountHistoryRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Folio/AccountHistoryResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Folio/AccountSummariesRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Folio/AccountSummariesResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Folio/AccountSummary.py +1 -1
- architect_py/{grpc_client → grpc/models}/Folio/AccountSummaryRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Folio/HistoricalFillsRequest.py +7 -4
- architect_py/{grpc_client → grpc/models}/Folio/HistoricalFillsResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Folio/HistoricalOrdersRequest.py +3 -3
- architect_py/{grpc_client → grpc/models}/Folio/HistoricalOrdersResponse.py +1 -1
- architect_py/grpc/models/Folio/__init__.py +2 -0
- architect_py/{grpc_client → grpc/models}/Health/HealthCheckRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Health/HealthCheckResponse.py +1 -1
- architect_py/grpc/models/Health/__init__.py +2 -0
- architect_py/{grpc_client → grpc/models}/Marketdata/Candle.py +1 -1
- architect_py/{grpc_client → grpc/models}/Marketdata/HistoricalCandlesRequest.py +11 -8
- architect_py/{grpc_client → grpc/models}/Marketdata/HistoricalCandlesResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Marketdata/L1BookSnapshot.py +52 -5
- architect_py/{grpc_client → grpc/models}/Marketdata/L1BookSnapshotRequest.py +8 -3
- architect_py/{grpc_client → grpc/models}/Marketdata/L1BookSnapshotsRequest.py +6 -3
- architect_py/{grpc_client → grpc/models}/Marketdata/L2BookSnapshot.py +1 -1
- architect_py/{grpc_client → grpc/models}/Marketdata/L2BookSnapshotRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/Liquidation.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/MarketStatus.py +1 -1
- architect_py/{grpc_client → grpc/models}/Marketdata/MarketStatusRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeCandlesRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeCurrentCandlesRequest.py +3 -4
- architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeL1BookSnapshotsRequest.py +6 -3
- architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeL2BookUpdatesRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeLiquidationsRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeManyCandlesRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeTickersRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeTradesRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/Ticker.py +14 -3
- architect_py/{grpc_client → grpc/models}/Marketdata/TickerRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/TickersRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/TickersResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Marketdata/Trade.py +2 -2
- architect_py/grpc/models/Marketdata/__init__.py +2 -0
- architect_py/grpc/models/Oms/Cancel.py +90 -0
- architect_py/{grpc_client → grpc/models}/Oms/CancelAllOrdersRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Oms/CancelAllOrdersResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Oms/CancelOrderRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Oms/OpenOrdersRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Oms/OpenOrdersResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Oms/Order.py +6 -13
- architect_py/{grpc_client → grpc/models}/Oms/PendingCancelsRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Oms/PendingCancelsResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Oms/PlaceOrderRequest.py +16 -23
- architect_py/grpc/models/Oms/__init__.py +2 -0
- architect_py/grpc/models/OptionsMarketdata/OptionsChain.py +30 -0
- architect_py/grpc/models/OptionsMarketdata/OptionsChainGreeks.py +30 -0
- architect_py/grpc/models/OptionsMarketdata/OptionsChainGreeksRequest.py +47 -0
- architect_py/grpc/models/OptionsMarketdata/OptionsChainRequest.py +45 -0
- architect_py/grpc/models/OptionsMarketdata/OptionsExpirations.py +29 -0
- architect_py/grpc/models/OptionsMarketdata/OptionsExpirationsRequest.py +42 -0
- architect_py/grpc/models/OptionsMarketdata/__init__.py +2 -0
- architect_py/{grpc_client → grpc/models}/Orderflow/DropcopyRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Orderflow/OrderflowRequest.py +2 -1
- architect_py/{grpc_client → grpc/models}/Orderflow/SubscribeOrderflowRequest.py +2 -2
- architect_py/grpc/models/Orderflow/__init__.py +2 -0
- architect_py/grpc/models/Symbology/DownloadProductCatalogRequest.py +42 -0
- architect_py/grpc/models/Symbology/DownloadProductCatalogResponse.py +27 -0
- architect_py/grpc/models/Symbology/ExecutionInfoRequest.py +47 -0
- architect_py/grpc/models/Symbology/ExecutionInfoResponse.py +27 -0
- architect_py/{grpc_client → grpc/models}/Symbology/PruneExpiredSymbolsRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Symbology/PruneExpiredSymbolsResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Symbology/SubscribeSymbology.py +1 -1
- architect_py/{grpc_client → grpc/models}/Symbology/SymbologyRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Symbology/SymbologySnapshot.py +7 -2
- architect_py/{grpc_client → grpc/models}/Symbology/SymbologyUpdate.py +9 -2
- architect_py/{grpc_client → grpc/models}/Symbology/SymbolsRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Symbology/SymbolsResponse.py +1 -1
- architect_py/grpc/models/Symbology/UploadProductCatalogRequest.py +49 -0
- architect_py/grpc/models/Symbology/UploadProductCatalogResponse.py +20 -0
- architect_py/{grpc_client → grpc/models}/Symbology/UploadSymbologyRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Symbology/UploadSymbologyResponse.py +1 -1
- architect_py/grpc/models/Symbology/__init__.py +2 -0
- architect_py/grpc/models/__init__.py +2 -0
- architect_py/{grpc_client → grpc/models}/definitions.py +671 -934
- architect_py/grpc/resolve_endpoint.py +70 -0
- architect_py/{grpc_client/grpc_server.py → grpc/server.py} +13 -9
- architect_py/grpc/utils.py +32 -0
- architect_py/internal_utils/__init__.py +0 -0
- architect_py/internal_utils/no_pandas.py +3 -0
- architect_py/tests/conftest.py +91 -85
- architect_py/tests/test_book_building.py +49 -50
- architect_py/tests/test_marketdata.py +168 -0
- architect_py/tests/test_order_entry.py +37 -0
- architect_py/tests/test_orderflow.py +41 -0
- architect_py/tests/test_portfolio_management.py +23 -0
- architect_py/tests/test_rounding.py +28 -28
- architect_py/tests/test_symbology.py +37 -30
- architect_py/utils/nearest_tick.py +2 -5
- architect_py/utils/nearest_tick_2.py +1 -2
- architect_py/utils/orderbook.py +35 -0
- architect_py/utils/pandas.py +44 -0
- architect_py/utils/price_bands.py +0 -3
- architect_py/utils/symbol_parsing.py +29 -0
- architect_py-5.0.0.dist-info/METADATA +54 -0
- architect_py-5.0.0.dist-info/RECORD +214 -0
- {architect_py-3.2.1.dist-info → architect_py-5.0.0.dist-info}/WHEEL +2 -1
- architect_py-5.0.0.dist-info/top_level.txt +4 -0
- examples/__init__.py +0 -0
- examples/book_subscription.py +52 -0
- examples/candles.py +30 -0
- examples/common.py +116 -0
- examples/external_cpty.py +77 -0
- examples/funding_rate_mean_reversion_algo.py +186 -0
- examples/order_sending.py +91 -0
- examples/stream_l1_marketdata.py +25 -0
- examples/stream_l2_marketdata.py +38 -0
- examples/trades.py +21 -0
- examples/tutorial_async.py +86 -0
- examples/tutorial_sync.py +95 -0
- scripts/generate_functions_md.py +166 -0
- scripts/generate_sync_interface.py +226 -0
- scripts/postprocess_grpc.py +604 -0
- scripts/preprocess_grpc_schema.py +708 -0
- templates/exceptions.py +83 -0
- templates/juniper_base_client.py +371 -0
- architect_py/client_protocol.py +0 -52
- architect_py/grpc_client/Algo/AlgoOrderForTwapAlgo.py +0 -61
- architect_py/grpc_client/Algo/CreateAlgoOrderRequestForTwapAlgo.py +0 -59
- architect_py/grpc_client/Algo/ModifyAlgoOrderRequestForTwapAlgo.py +0 -45
- architect_py/grpc_client/Folio/AggregatedAccountSummariesRequest.py +0 -59
- architect_py/grpc_client/Folio/AggregatedAccountSummariesResponse.py +0 -27
- architect_py/grpc_client/Health/__init__.py +0 -2
- architect_py/grpc_client/Marketdata/__init__.py +0 -2
- architect_py/grpc_client/Oms/Cancel.py +0 -42
- architect_py/grpc_client/Oms/__init__.py +0 -2
- architect_py/grpc_client/Orderflow/__init__.py +0 -2
- architect_py/grpc_client/Symbology/__init__.py +0 -2
- architect_py/grpc_client/__init__.py +0 -2
- architect_py/grpc_client/grpc_client.py +0 -405
- architect_py/scalars.py +0 -172
- architect_py/tests/test_accounts.py +0 -31
- architect_py/tests/test_client.py +0 -29
- architect_py/tests/test_grpc_client.py +0 -30
- architect_py/tests/test_order_sending.py +0 -61
- architect_py/tests/test_snapshots.py +0 -52
- architect_py/tests/test_subscriptions.py +0 -129
- architect_py-3.2.1.dist-info/METADATA +0 -212
- architect_py-3.2.1.dist-info/RECORD +0 -146
- /architect_py/{grpc_client → grpc/models}/Marketdata/ArrayOfL1BookSnapshot.py +0 -0
- /architect_py/{grpc_client → grpc/models}/Marketdata/L2BookUpdate.py +0 -0
- /architect_py/{grpc_client → grpc/models}/Marketdata/TickerUpdate.py +0 -0
- /architect_py/{grpc_client → grpc/models}/Orderflow/Dropcopy.py +0 -0
- /architect_py/{grpc_client → grpc/models}/Orderflow/Orderflow.py +0 -0
- {architect_py-3.2.1.dist-info → architect_py-5.0.0.dist-info/licenses}/LICENSE +0 -0
architect_py/async_client.py
CHANGED
@@ -1,208 +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
|
114
|
-
|
115
|
-
the API key and secret can be generated on the app.architect.co website
|
116
|
-
|
117
|
-
Returns:
|
118
|
-
Client object
|
84
|
+
Connect to an Architect installation.
|
119
85
|
|
120
|
-
Raises
|
121
|
-
|
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 create 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,
|
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)
|
251
|
+
|
252
|
+
async def set_marketdata(self, venue: Venue, endpoint: str):
|
253
|
+
"""
|
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")
|
301
|
+
|
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)
|
326
|
+
"""
|
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"
|
200
336
|
)
|
201
337
|
|
202
338
|
# ------------------------------------------------------------
|
203
339
|
# Symbology
|
204
340
|
# ------------------------------------------------------------
|
205
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
|
+
|
206
360
|
async def search_symbols(
|
207
361
|
self,
|
208
362
|
search_string: Optional[str] = None,
|
@@ -211,77 +365,74 @@ class AsyncClient:
|
|
211
365
|
limit: int = 20,
|
212
366
|
) -> List[TradableProduct]:
|
213
367
|
"""
|
214
|
-
Search for
|
368
|
+
Search for tradable products on Architect.
|
215
369
|
|
216
370
|
Args:
|
217
|
-
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.
|
218
373
|
Examples: "ES", "NQ", "GC"
|
219
374
|
execution_venue: the execution venue to search in
|
220
375
|
Examples: "CME"
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
limit=limit,
|
230
|
-
)
|
231
|
-
).search_symbols
|
232
|
-
|
233
|
-
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
|
234
384
|
|
235
385
|
async def get_product_info(self, symbol: str) -> Optional[ProductInfoFields]:
|
236
386
|
"""
|
237
|
-
Get
|
387
|
+
Get information about a product, e.g. product_type, underlying, multiplier.
|
238
388
|
|
239
389
|
Args:
|
240
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
|
+
|
241
396
|
Returns:
|
242
|
-
|
397
|
+
None if the symbol does not exist
|
243
398
|
"""
|
244
|
-
|
245
|
-
return
|
399
|
+
res = await self.graphql_client.get_product_info_query(symbol)
|
400
|
+
return res.product_info
|
246
401
|
|
247
|
-
@functools.lru_cache
|
248
402
|
async def get_product_infos(
|
249
403
|
self, symbols: Optional[list[str]]
|
250
404
|
) -> Sequence[ProductInfoFields]:
|
251
405
|
"""
|
252
|
-
Get
|
406
|
+
Get information about products, e.g. product_type, underlying, multiplier.
|
253
407
|
|
254
408
|
Args:
|
255
|
-
symbols: the symbols to get information for
|
256
|
-
Returns:
|
257
|
-
a list of ProductInfoFields
|
409
|
+
symbols: the symbols to get information for, or None for all symbols
|
258
410
|
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
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.
|
263
415
|
"""
|
264
|
-
|
265
|
-
return
|
416
|
+
res = await self.graphql_client.get_product_infos_query(symbols)
|
417
|
+
return res.product_infos
|
266
418
|
|
267
|
-
@functools.lru_cache
|
268
419
|
async def get_execution_info(
|
269
|
-
self, symbol: TradableProduct, execution_venue: str
|
420
|
+
self, symbol: TradableProduct | str, execution_venue: str
|
270
421
|
) -> Optional[ExecutionInfoFields]:
|
271
422
|
"""
|
272
|
-
Get
|
423
|
+
Get information about tradable product execution, e.g. tick_size,
|
424
|
+
step_size, margins.
|
273
425
|
|
274
426
|
Args:
|
275
427
|
symbol: the symbol to get execution information for
|
276
428
|
execution_venue: the execution venue e.g. "CME"
|
277
429
|
|
278
430
|
Returns:
|
279
|
-
|
280
|
-
|
431
|
+
None if the symbol doesn't exist
|
281
432
|
"""
|
282
433
|
try:
|
283
434
|
execution_info = await self.graphql_client.get_execution_info_query(
|
284
|
-
symbol, execution_venue
|
435
|
+
TradableProduct(symbol), execution_venue
|
285
436
|
)
|
286
437
|
return execution_info.execution_info
|
287
438
|
except GraphQLClientGraphQLMultiError:
|
@@ -291,63 +442,73 @@ class AsyncClient:
|
|
291
442
|
|
292
443
|
async def get_execution_infos(
|
293
444
|
self,
|
294
|
-
symbols: Optional[list[TradableProduct]],
|
445
|
+
symbols: Optional[list[TradableProduct | str]],
|
295
446
|
execution_venue: Optional[str] = None,
|
296
447
|
) -> Sequence[ExecutionInfoFields]:
|
297
448
|
"""
|
298
|
-
Get
|
449
|
+
Get information about tradable product execution, e.g. tick_size,
|
450
|
+
step_size, margins, for many symbols.
|
299
451
|
|
300
452
|
Args:
|
301
|
-
symbols: the symbols to get execution information for
|
453
|
+
symbols: the symbols to get execution information for, or None for all symbols
|
302
454
|
execution_venue: the execution venue e.g. "CME"
|
303
455
|
|
304
456
|
Returns:
|
305
|
-
|
306
|
-
|
307
|
-
|
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.
|
308
460
|
"""
|
309
|
-
|
310
|
-
symbols
|
311
|
-
|
312
|
-
|
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
|
313
467
|
|
314
468
|
async def get_cme_first_notice_date(self, symbol: str) -> Optional[date]:
|
315
469
|
"""
|
470
|
+
@deprecated(reason="Use get_product_info instead; first_notice_date is now a field")
|
471
|
+
|
316
472
|
Get the first notice date for a CME future.
|
317
473
|
|
318
474
|
Args:
|
319
475
|
symbol: the symbol to get the first notice date for a CME future
|
320
476
|
|
321
477
|
Returns:
|
322
|
-
|
478
|
+
The first notice date as a date object if it exists
|
323
479
|
"""
|
324
|
-
|
325
|
-
if
|
480
|
+
res = await self.graphql_client.get_first_notice_date_query(symbol)
|
481
|
+
if res is None or res.product_info is None:
|
326
482
|
return None
|
327
|
-
return
|
483
|
+
return res.product_info.first_notice_date
|
328
484
|
|
329
485
|
async def get_future_series(self, series_symbol: str) -> list[str]:
|
330
486
|
"""
|
331
|
-
|
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.
|
332
495
|
|
333
496
|
Args:
|
334
|
-
series_symbol: the
|
335
|
-
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
|
336
499
|
Returns:
|
337
|
-
|
500
|
+
List of futures products
|
338
501
|
"""
|
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
|
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
|
347
506
|
|
348
507
|
@staticmethod
|
349
|
-
def get_expiration_from_CME_name(name: str) -> date:
|
508
|
+
def get_expiration_from_CME_name(name: str) -> Optional[date]:
|
350
509
|
"""
|
510
|
+
@deprecated(reason="Use utils.symbol_parsing.nominative_expiration instead")
|
511
|
+
|
351
512
|
Get the expiration date from a CME future name.
|
352
513
|
|
353
514
|
Args:
|
@@ -356,12 +517,13 @@ class AsyncClient:
|
|
356
517
|
Returns:
|
357
518
|
the expiration date as a date object
|
358
519
|
"""
|
359
|
-
|
360
|
-
return datetime.strptime(d, "%Y%m%d").date()
|
520
|
+
return nominative_expiration(name)
|
361
521
|
|
362
522
|
async def get_cme_futures_series(self, series: str) -> list[tuple[date, str]]:
|
363
523
|
"""
|
364
|
-
|
524
|
+
@deprecated(reason="Use get_futures_series instead")
|
525
|
+
|
526
|
+
List all futures in a given CME series.
|
365
527
|
|
366
528
|
Args:
|
367
529
|
series: the series to get the futures for
|
@@ -371,143 +533,547 @@ class AsyncClient:
|
|
371
533
|
the symbol for each future in the series
|
372
534
|
|
373
535
|
e.g.
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
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
|
+
# ...
|
378
542
|
]
|
543
|
+
```
|
544
|
+
"""
|
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
|
553
|
+
|
554
|
+
async def get_cme_future_from_root_month_year(
|
555
|
+
self, root: str, month: int, year: int
|
556
|
+
) -> str:
|
379
557
|
"""
|
380
|
-
|
381
|
-
|
558
|
+
Get the symbol for a CME future from the root, month, and year.
|
559
|
+
This is a simple wrapper around search_symbols.
|
560
|
+
|
561
|
+
Args:
|
562
|
+
root: the root symbol for the future e.g. "ES"
|
563
|
+
month: the month of the future
|
564
|
+
year: the year of the future
|
565
|
+
Returns:
|
566
|
+
The future symbol if it exists and is unique.
|
567
|
+
"""
|
568
|
+
[market] = [
|
569
|
+
market
|
570
|
+
for market in await self.search_symbols(
|
571
|
+
f"{root} {year}{month:02d}",
|
572
|
+
execution_venue="CME",
|
573
|
+
)
|
574
|
+
if market.startswith(f"{root} {year}{month:02d}")
|
575
|
+
]
|
576
|
+
return market
|
577
|
+
|
578
|
+
# ------------------------------------------------------------
|
579
|
+
# Marketdata
|
580
|
+
# ------------------------------------------------------------
|
581
|
+
|
582
|
+
async def get_market_status(
|
583
|
+
self, symbol: TradableProduct | str, venue: Venue
|
584
|
+
) -> MarketStatus:
|
585
|
+
"""
|
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")
|
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
|
608
|
+
Returns:
|
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]
|
382
628
|
)
|
383
629
|
|
384
|
-
|
385
|
-
|
386
|
-
|
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]
|
387
870
|
|
388
|
-
|
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.
|
389
878
|
|
390
|
-
|
879
|
+
If a subscription is already active, the existing reference will be
|
880
|
+
returned; otherwise, a new subscription will be created.
|
391
881
|
|
392
|
-
|
393
|
-
self, root: str, month: int, year: int
|
394
|
-
) -> str:
|
395
|
-
"""
|
396
|
-
Get the symbol for a CME future from the root, month, and year.
|
882
|
+
Snapshots will have an initial value of timestamp=0 and bids/asks=[].
|
397
883
|
|
398
884
|
Args:
|
399
|
-
|
400
|
-
|
401
|
-
year: the year of the future
|
402
|
-
Returns:
|
403
|
-
the symbol for the future
|
885
|
+
symbol: the symbol to subscribe to
|
886
|
+
venue: the marketdata venue
|
404
887
|
|
405
|
-
|
406
|
-
|
888
|
+
Return:
|
889
|
+
An L2 book object that is constantly updating in the background.
|
407
890
|
"""
|
408
|
-
|
409
|
-
market
|
410
|
-
for market in await self.search_symbols(
|
411
|
-
f"{root} {year}{month:02d}",
|
412
|
-
execution_venue="CME",
|
413
|
-
)
|
414
|
-
if market.startswith(f"{root} {year}{month:02d}")
|
415
|
-
]
|
891
|
+
symbol = TradableProduct(symbol)
|
416
892
|
|
417
|
-
|
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
|
418
908
|
|
419
|
-
|
420
|
-
|
421
|
-
|
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]
|
422
951
|
|
423
|
-
async def
|
952
|
+
async def stream_trades(
|
953
|
+
self, symbol: TradableProduct | str, venue: Venue
|
954
|
+
) -> AsyncGenerator[Trade, None]:
|
424
955
|
"""
|
425
|
-
|
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
|
426
962
|
|
427
|
-
|
428
|
-
|
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.
|
429
971
|
"""
|
430
|
-
|
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
|
431
980
|
|
432
|
-
|
981
|
+
# ------------------------------------------------------------
|
982
|
+
# Portfolio management
|
983
|
+
# ------------------------------------------------------------
|
433
984
|
|
434
|
-
async def list_accounts(self) ->
|
985
|
+
async def list_accounts(self) -> List[AccountWithPermissions]:
|
435
986
|
"""
|
436
987
|
List accounts for the user that the API key belongs to.
|
437
988
|
|
438
989
|
Returns:
|
439
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
|
440
992
|
(use who_am_i to get the user_id / email)
|
441
993
|
"""
|
442
|
-
|
443
|
-
|
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
|
444
998
|
|
445
|
-
async def get_account_summary(self, account: str) ->
|
999
|
+
async def get_account_summary(self, account: str) -> AccountSummary:
|
446
1000
|
"""
|
447
|
-
|
1001
|
+
Get account summary, including balances, positions, pnls, etc.
|
448
1002
|
|
449
1003
|
Args:
|
450
|
-
account:
|
451
|
-
|
452
|
-
Returns:
|
453
|
-
AccountSummaryFields for the account
|
1004
|
+
account: account uuid or name
|
1005
|
+
Examples: "00000000-0000-0000-0000-000000000000", "STONEX:000000/JDoe"
|
454
1006
|
"""
|
455
|
-
|
456
|
-
|
1007
|
+
grpc_client = await self.core()
|
1008
|
+
req = AccountSummaryRequest(account=account)
|
1009
|
+
res = await grpc_client.unary_unary(req)
|
1010
|
+
return res
|
457
1011
|
|
458
1012
|
async def get_account_summaries(
|
459
1013
|
self,
|
460
1014
|
accounts: Optional[list[str]] = None,
|
461
1015
|
trader: Optional[str] = None,
|
462
|
-
) ->
|
1016
|
+
) -> list[AccountSummary]:
|
463
1017
|
"""
|
464
|
-
|
1018
|
+
Get account summaries for accounts matching the filters.
|
465
1019
|
|
466
1020
|
Args:
|
467
|
-
accounts:
|
468
|
-
|
469
|
-
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
|
470
1023
|
|
471
|
-
|
472
|
-
Returns:
|
473
|
-
a list of AccountSummary for the accounts
|
1024
|
+
If both arguments are given, the union of matching accounts are returned.
|
474
1025
|
"""
|
475
|
-
|
476
|
-
|
1026
|
+
grpc_client = await self.core()
|
1027
|
+
request = AccountSummariesRequest(
|
1028
|
+
accounts=accounts,
|
1029
|
+
trader=trader,
|
477
1030
|
)
|
478
|
-
|
1031
|
+
res = await grpc_client.unary_unary(request)
|
1032
|
+
return res.account_summaries
|
479
1033
|
|
480
1034
|
async def get_account_history(
|
481
1035
|
self,
|
482
1036
|
account: str,
|
483
1037
|
from_inclusive: Optional[datetime] = None,
|
484
1038
|
to_exclusive: Optional[datetime] = None,
|
485
|
-
) ->
|
1039
|
+
) -> list[AccountSummary]:
|
486
1040
|
"""
|
487
|
-
|
488
|
-
|
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
|
1041
|
+
Get historical sequence of account summaries for the given account.
|
492
1042
|
"""
|
493
|
-
|
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(
|
494
1059
|
account=account, from_inclusive=from_inclusive, to_exclusive=to_exclusive
|
495
1060
|
)
|
496
|
-
|
1061
|
+
res = await grpc_client.unary_unary(req)
|
1062
|
+
return res.history
|
497
1063
|
|
498
1064
|
# ------------------------------------------------------------
|
499
|
-
# Order
|
1065
|
+
# Order management
|
500
1066
|
# ------------------------------------------------------------
|
501
1067
|
|
502
1068
|
async def get_open_orders(
|
503
1069
|
self,
|
504
|
-
order_ids: Optional[list[
|
1070
|
+
order_ids: Optional[list[OrderId]] = None,
|
505
1071
|
venue: Optional[str] = None,
|
506
1072
|
account: Optional[str] = None,
|
507
1073
|
trader: Optional[str] = None,
|
508
1074
|
symbol: Optional[str] = None,
|
509
|
-
parent_order_id: Optional[
|
510
|
-
) ->
|
1075
|
+
parent_order_id: Optional[OrderId] = None,
|
1076
|
+
) -> list[Order]:
|
511
1077
|
"""
|
512
1078
|
Returns a list of open orders for the user that match the filters.
|
513
1079
|
|
@@ -519,12 +1085,11 @@ class AsyncClient:
|
|
519
1085
|
symbol: the symbol to get orders for
|
520
1086
|
parent_order_id: the parent order id to get orders for
|
521
1087
|
|
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
1088
|
Returns:
|
525
|
-
|
1089
|
+
Open orders that match the union of the filters
|
526
1090
|
"""
|
527
|
-
|
1091
|
+
grpc_client = await self.core()
|
1092
|
+
open_orders_request = OpenOrdersRequest(
|
528
1093
|
venue=venue,
|
529
1094
|
account=account,
|
530
1095
|
trader=trader,
|
@@ -532,46 +1097,63 @@ class AsyncClient:
|
|
532
1097
|
parent_order_id=parent_order_id,
|
533
1098
|
order_ids=order_ids,
|
534
1099
|
)
|
535
|
-
return orders.open_orders
|
536
1100
|
|
537
|
-
|
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]:
|
538
1105
|
"""
|
539
|
-
|
1106
|
+
@deprecated(reason="Use get_open_orders with no parameters instead")
|
540
1107
|
|
541
|
-
Returns
|
542
|
-
a list of OrderFields of all the open orders for the user
|
1108
|
+
Returns a list of all open orders for the authenticated user.
|
543
1109
|
"""
|
544
|
-
|
545
|
-
return orders.open_orders
|
1110
|
+
return await self.get_open_orders()
|
546
1111
|
|
547
1112
|
async def get_historical_orders(
|
548
1113
|
self,
|
549
|
-
order_ids: Optional[list[
|
1114
|
+
order_ids: Optional[list[OrderId]] = None,
|
550
1115
|
from_inclusive: Optional[datetime] = None,
|
551
1116
|
to_exclusive: Optional[datetime] = None,
|
552
1117
|
venue: Optional[str] = None,
|
553
1118
|
account: Optional[str] = None,
|
554
|
-
parent_order_id: Optional[
|
555
|
-
) ->
|
1119
|
+
parent_order_id: Optional[OrderId] = None,
|
1120
|
+
) -> list[Order]:
|
556
1121
|
"""
|
557
|
-
|
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.
|
558
1126
|
|
559
1127
|
Args:
|
560
1128
|
order_ids: a list of order ids to get
|
561
1129
|
from_inclusive: the start date to get orders for
|
562
1130
|
to_exclusive: the end date to get orders for
|
563
1131
|
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)
|
1132
|
+
account: account uuid or name
|
566
1133
|
parent_order_id: the parent order id to get orders for
|
567
1134
|
Returns:
|
568
|
-
|
1135
|
+
Historical orders that match the union of the filters.
|
569
1136
|
|
570
|
-
|
571
|
-
|
572
|
-
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.
|
573
1139
|
"""
|
574
|
-
|
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(
|
575
1157
|
order_ids=order_ids,
|
576
1158
|
venue=venue,
|
577
1159
|
account=account,
|
@@ -579,405 +1161,221 @@ class AsyncClient:
|
|
579
1161
|
from_inclusive=from_inclusive,
|
580
1162
|
to_exclusive=to_exclusive,
|
581
1163
|
)
|
582
|
-
|
1164
|
+
orders = await grpc_client.unary_unary(historical_orders_request)
|
1165
|
+
return orders.orders
|
583
1166
|
|
584
|
-
async def get_order(self, order_id:
|
1167
|
+
async def get_order(self, order_id: OrderId) -> Optional[Order]:
|
585
1168
|
"""
|
586
|
-
Returns the
|
587
|
-
|
1169
|
+
Returns the specified order. Useful for looking at past sent orders.
|
1170
|
+
Queries open_orders first, then queries historical_orders.
|
588
1171
|
|
589
1172
|
Args:
|
590
1173
|
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
1174
|
"""
|
596
|
-
|
597
|
-
|
1175
|
+
grpc_client = await self.core()
|
1176
|
+
req = OpenOrdersRequest.new(
|
1177
|
+
order_ids=[order_id],
|
598
1178
|
)
|
599
|
-
|
600
|
-
for open_order in
|
1179
|
+
res = await grpc_client.unary_unary(req)
|
1180
|
+
for open_order in res.open_orders:
|
601
1181
|
if open_order.id == order_id:
|
602
1182
|
return open_order
|
603
1183
|
|
604
|
-
|
605
|
-
order_ids=[order_id]
|
1184
|
+
req = HistoricalOrdersRequest.new(
|
1185
|
+
order_ids=[order_id],
|
606
1186
|
)
|
1187
|
+
res = await grpc_client.unary_unary(req)
|
1188
|
+
if res.orders and len(res.orders) == 1:
|
1189
|
+
return res.orders[0]
|
607
1190
|
|
608
|
-
|
609
|
-
return historical_orders.historical_orders[0]
|
610
|
-
|
611
|
-
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]]:
|
612
1192
|
"""
|
613
|
-
Returns
|
614
|
-
|
1193
|
+
Returns the specified orders. Useful for looking at past sent orders.
|
1194
|
+
Plural form of get_order.
|
615
1195
|
|
616
1196
|
Args:
|
617
1197
|
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
1198
|
"""
|
623
|
-
|
1199
|
+
grpc_client = await self.core()
|
1200
|
+
orders_dict: dict[OrderId, Optional[Order]] = {
|
624
1201
|
order_id: None for order_id in order_ids
|
625
1202
|
}
|
1203
|
+
req = OpenOrdersRequest.new(
|
1204
|
+
order_ids=order_ids,
|
1205
|
+
)
|
626
1206
|
|
627
|
-
|
628
|
-
|
629
|
-
).open_orders
|
630
|
-
for open_order in open_orders:
|
1207
|
+
res = await grpc_client.unary_unary(req)
|
1208
|
+
for open_order in res.open_orders:
|
631
1209
|
orders_dict[open_order.id] = open_order
|
632
1210
|
|
633
1211
|
not_open_order_ids = [
|
634
1212
|
order_id for order_id in order_ids if orders_dict[order_id] is None
|
635
1213
|
]
|
636
1214
|
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
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:
|
643
1220
|
orders_dict[historical_order.id] = historical_order
|
644
1221
|
|
645
1222
|
return [orders_dict[order_id] for order_id in order_ids]
|
646
1223
|
|
647
1224
|
async def get_fills(
|
648
1225
|
self,
|
649
|
-
from_inclusive: Optional[datetime],
|
650
|
-
to_exclusive: Optional[datetime],
|
1226
|
+
from_inclusive: Optional[datetime] = None,
|
1227
|
+
to_exclusive: Optional[datetime] = None,
|
651
1228
|
venue: Optional[str] = None,
|
652
1229
|
account: Optional[str] = None,
|
653
|
-
order_id: Optional[
|
654
|
-
|
1230
|
+
order_id: Optional[OrderId] = None,
|
1231
|
+
limit: Optional[int] = None,
|
1232
|
+
) -> HistoricalFillsResponse:
|
655
1233
|
"""
|
656
|
-
Returns
|
1234
|
+
Returns all fills matching the given filters.
|
657
1235
|
|
658
1236
|
Args:
|
659
1237
|
from_inclusive: the start date to get fills for
|
660
1238
|
to_exclusive: the end date to get fills for
|
661
1239
|
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)
|
1240
|
+
account: account uuid or name
|
664
1241
|
order_id: the order id to get fills for
|
665
|
-
Returns:
|
666
|
-
a list of GetFillsQueryFolioHistoricalFills
|
667
|
-
"""
|
668
|
-
fills = await self.graphql_client.get_fills_query(
|
669
|
-
venue, account, order_id, from_inclusive, to_exclusive
|
670
|
-
)
|
671
|
-
return fills.historical_fills
|
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
|
691
|
-
|
692
|
-
async def get_market_snapshot(
|
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(
|
723
|
-
self,
|
724
|
-
symbol: str,
|
725
|
-
candle_width: grpc_definitions.CandleWidth,
|
726
|
-
start: datetime,
|
727
|
-
end: datetime,
|
728
|
-
) -> HistoricalCandlesResponse:
|
729
1242
|
"""
|
730
|
-
|
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
|
+
)
|
731
1250
|
|
732
|
-
|
733
|
-
|
734
|
-
|
735
|
-
|
736
|
-
|
737
|
-
|
738
|
-
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
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,
|
746
1264
|
)
|
1265
|
+
res = await grpc_client.unary_unary(req)
|
1266
|
+
return res
|
747
1267
|
|
748
|
-
|
749
|
-
|
750
|
-
async def get_l1_book_snapshot(
|
1268
|
+
async def orderflow(
|
751
1269
|
self,
|
752
|
-
|
753
|
-
|
754
|
-
) -> MarketTickerFields:
|
1270
|
+
request_iterator: AsyncIterator[OrderflowRequest],
|
1271
|
+
) -> AsyncGenerator[Orderflow, None]:
|
755
1272
|
"""
|
756
|
-
|
1273
|
+
A two-way channel for both order entry and listening to order updates (fills, acks, outs, etc.).
|
757
1274
|
|
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
|
763
|
-
"""
|
764
|
-
snapshot = await self.graphql_client.get_l_1_book_snapshot_query(
|
765
|
-
symbol=symbol, venue=venue
|
766
|
-
)
|
767
|
-
return snapshot.ticker
|
1275
|
+
This is considered the most efficient way to trade in this SDK.
|
768
1276
|
|
769
|
-
|
770
|
-
|
771
|
-
) -> Sequence[MarketTickerFields]:
|
772
|
-
"""
|
773
|
-
Gets the L1 book snapshots for a list of symbols.
|
1277
|
+
Example:
|
1278
|
+
See test_orderflow.py for an example.
|
774
1279
|
|
775
|
-
|
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
|
1280
|
+
This WILL block the event loop until the stream is closed.
|
780
1281
|
"""
|
781
|
-
|
782
|
-
|
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,
|
783
1288
|
)
|
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.
|
789
|
-
|
790
|
-
Args:
|
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
|
1289
|
+
call: grpc.aio._base_call.StreamStreamCall[OrderflowRequest, Orderflow] = stub(
|
1290
|
+
request_iterator, metadata=(("authorization", f"Bearer {grpc_client.jwt}"),)
|
801
1291
|
)
|
802
|
-
|
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
|
1292
|
+
async for update in call:
|
1293
|
+
yield update
|
877
1294
|
|
878
|
-
async def
|
1295
|
+
async def stream_orderflow(
|
879
1296
|
self,
|
880
|
-
|
881
|
-
|
882
|
-
|
1297
|
+
account: Optional[AccountIdOrName] = None,
|
1298
|
+
execution_venue: Optional[str] = None,
|
1299
|
+
trader: Optional[TraderIdOrEmail] = None,
|
1300
|
+
) -> AsyncGenerator[Orderflow, None]:
|
883
1301
|
"""
|
884
|
-
|
885
|
-
|
886
|
-
Args:
|
887
|
-
symbols: the symbols to subscribe to
|
888
|
-
Return:
|
889
|
-
a list of L2BookSnapshot object that is constantly updating in the background
|
890
|
-
For the duration of the program, the client will be subscribed to the stream
|
891
|
-
and be updating the L2BookSnapshot.
|
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
|
1302
|
+
A stream for listening to order updates (fills, acks, outs, etc.).
|
919
1303
|
|
920
|
-
|
921
|
-
|
922
|
-
|
923
|
-
|
924
|
-
|
925
|
-
|
926
|
-
request = SubscribeTradesRequest(symbol=symbol, venue=venue)
|
927
|
-
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
|
+
```
|
928
1310
|
|
929
|
-
|
930
|
-
self,
|
931
|
-
symbol: TradableProduct,
|
932
|
-
venue: Optional[str],
|
933
|
-
candle_widths: Optional[list[grpc_definitions.CandleWidth]],
|
934
|
-
) -> AsyncIterator[Candle]:
|
1311
|
+
This WILL block the event loop until the stream is closed.
|
935
1312
|
"""
|
936
|
-
|
937
|
-
|
938
|
-
|
939
|
-
|
940
|
-
|
941
|
-
|
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,
|
942
1325
|
)
|
943
|
-
|
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
|
944
1331
|
|
945
1332
|
# ------------------------------------------------------------
|
946
|
-
# Order
|
1333
|
+
# Order entry
|
947
1334
|
# ------------------------------------------------------------
|
948
1335
|
|
949
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(
|
950
1347
|
self,
|
951
1348
|
*,
|
952
|
-
|
953
|
-
|
954
|
-
|
1349
|
+
id: Optional[OrderId] = None,
|
1350
|
+
symbol: TradableProduct | str,
|
1351
|
+
execution_venue: Optional[str] = None,
|
1352
|
+
dir: Optional[OrderDir] = None,
|
955
1353
|
quantity: Decimal,
|
956
1354
|
limit_price: Decimal,
|
957
1355
|
order_type: OrderType = OrderType.LIMIT,
|
958
|
-
time_in_force: TimeInForce =
|
959
|
-
good_til_date: Optional[datetime] = None,
|
1356
|
+
time_in_force: TimeInForce = TimeInForceEnum.DAY,
|
960
1357
|
price_round_method: Optional[TickRoundMethod] = None,
|
961
1358
|
account: Optional[str] = None,
|
962
1359
|
trader: Optional[str] = None,
|
963
1360
|
post_only: bool = False,
|
964
1361
|
trigger_price: Optional[Decimal] = None,
|
965
|
-
|
1362
|
+
**kwargs: Any,
|
1363
|
+
) -> Order:
|
966
1364
|
"""
|
967
1365
|
Sends a regular limit order.
|
968
1366
|
|
969
1367
|
Args:
|
1368
|
+
id: in case user wants to generate their own order id, otherwise it will be generated automatically
|
970
1369
|
symbol: the symbol to send the order for
|
971
1370
|
execution_venue: the execution venue to send the order to,
|
972
1371
|
if execution_venue is set to None, the OMS will send the order to the primary_exchange
|
973
1372
|
the primary_exchange can be deduced from `get_product_info`
|
974
|
-
|
1373
|
+
dir: the direction of the order, BUY or SELL
|
975
1374
|
quantity: the quantity of the order
|
976
1375
|
limit_price: the limit price of the order
|
977
1376
|
It is highly recommended to make this a Decimal object from the decimal module to avoid floating point errors
|
978
1377
|
order_type: the type of the order
|
979
1378
|
time_in_force: the time in force of the order
|
980
|
-
good_til_date: the date the order is good until, only relevant for time_in_force = "GTD"
|
981
1379
|
price_round_method: the method to round the price to the nearest tick, will not round if None
|
982
1380
|
account: the account to send the order for
|
983
1381
|
While technically optional, for most order types, the account is required
|
@@ -986,13 +1384,21 @@ class AsyncClient:
|
|
986
1384
|
post_only: whether the order should be post only, not supported by all exchanges
|
987
1385
|
trigger_price: the trigger price for the order, only relevant for stop / take_profit orders
|
988
1386
|
Returns:
|
989
|
-
the
|
1387
|
+
the Order object for the order
|
990
1388
|
The order.status should be "PENDING" until the order is "OPEN" / "REJECTED" / "OUT" / "CANCELED" / "STALE"
|
991
1389
|
|
992
1390
|
If the order is rejected, the order.reject_reason and order.reject_message will be set
|
993
1391
|
"""
|
1392
|
+
grpc_client = await self.core()
|
994
1393
|
assert quantity > 0, "quantity must be positive"
|
995
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
|
+
|
996
1402
|
if price_round_method is not None:
|
997
1403
|
if execution_venue is None:
|
998
1404
|
product_info = await self.get_product_info(symbol)
|
@@ -1018,43 +1424,43 @@ class AsyncClient:
|
|
1018
1424
|
else:
|
1019
1425
|
raise ValueError(f"Could not find market information for {symbol}")
|
1020
1426
|
|
1021
|
-
|
1022
|
-
|
1023
|
-
|
1024
|
-
|
1025
|
-
|
1026
|
-
|
1027
|
-
|
1028
|
-
|
1029
|
-
|
1030
|
-
None,
|
1031
|
-
|
1032
|
-
|
1033
|
-
|
1034
|
-
post_only,
|
1035
|
-
trigger_price,
|
1036
|
-
good_til_date,
|
1037
|
-
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,
|
1038
1442
|
)
|
1039
|
-
|
1040
|
-
return
|
1443
|
+
res = await grpc_client.unary_unary(req)
|
1444
|
+
return res
|
1041
1445
|
|
1042
1446
|
async def send_market_pro_order(
|
1043
1447
|
self,
|
1044
1448
|
*,
|
1045
|
-
|
1449
|
+
id: Optional[OrderId] = None,
|
1450
|
+
symbol: TradableProduct | str,
|
1046
1451
|
execution_venue: str,
|
1047
1452
|
odir: OrderDir,
|
1048
1453
|
quantity: Decimal,
|
1049
|
-
time_in_force: TimeInForce =
|
1454
|
+
time_in_force: TimeInForce = TimeInForceEnum.DAY,
|
1050
1455
|
account: Optional[str] = None,
|
1051
1456
|
fraction_through_market: Decimal = Decimal("0.001"),
|
1052
|
-
) ->
|
1457
|
+
) -> Order:
|
1053
1458
|
"""
|
1054
1459
|
Sends a market-order like limit price based on the BBO.
|
1055
1460
|
Meant to behave as a market order but with more protections.
|
1056
1461
|
|
1057
1462
|
Args:
|
1463
|
+
id: in case user wants to generate their own order id, otherwise it will be generated automatically
|
1058
1464
|
symbol: the symbol to send the order for
|
1059
1465
|
execution_venue: the execution venue to send the order to
|
1060
1466
|
odir: the direction of the order
|
@@ -1070,36 +1476,33 @@ class AsyncClient:
|
|
1070
1476
|
If the order is rejected, the order.reject_reason and order.reject_message will be set
|
1071
1477
|
"""
|
1072
1478
|
|
1073
|
-
|
1074
|
-
|
1075
|
-
symbol=symbol, venue=execution_venue
|
1076
|
-
)
|
1077
|
-
if bbo_snapshot is None:
|
1479
|
+
ticker = await self.get_ticker(symbol, execution_venue)
|
1480
|
+
if ticker is None:
|
1078
1481
|
raise ValueError(
|
1079
|
-
f"Failed to send market order with reason: no
|
1482
|
+
f"Failed to send market order with reason: no ticker for {symbol}"
|
1080
1483
|
)
|
1081
1484
|
|
1082
1485
|
price_band = price_band_pairs.get(symbol, None)
|
1083
1486
|
|
1084
1487
|
if odir == OrderDir.BUY:
|
1085
|
-
if
|
1488
|
+
if ticker.ask_price is None:
|
1086
1489
|
raise ValueError(
|
1087
1490
|
f"Failed to send market order with reason: no ask price for {symbol}"
|
1088
1491
|
)
|
1089
|
-
limit_price =
|
1492
|
+
limit_price = ticker.ask_price * (1 + fraction_through_market)
|
1090
1493
|
|
1091
|
-
if price_band and
|
1092
|
-
price_band_reference_price =
|
1494
|
+
if price_band and ticker.last_price:
|
1495
|
+
price_band_reference_price = ticker.last_price + price_band
|
1093
1496
|
limit_price = min(limit_price, price_band_reference_price)
|
1094
1497
|
|
1095
1498
|
else:
|
1096
|
-
if
|
1499
|
+
if ticker.bid_price is None:
|
1097
1500
|
raise ValueError(
|
1098
1501
|
f"Failed to send market order with reason: no bid price for {symbol}"
|
1099
1502
|
)
|
1100
|
-
limit_price =
|
1101
|
-
if price_band and
|
1102
|
-
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
|
1103
1506
|
limit_price = min(limit_price, price_band_reference_price)
|
1104
1507
|
|
1105
1508
|
# Conservatively round price to nearest tick
|
@@ -1117,7 +1520,8 @@ class AsyncClient:
|
|
1117
1520
|
):
|
1118
1521
|
limit_price = tick_round_method(limit_price, tick_size)
|
1119
1522
|
|
1120
|
-
return await self.
|
1523
|
+
return await self.place_limit_order(
|
1524
|
+
id=id,
|
1121
1525
|
symbol=symbol,
|
1122
1526
|
execution_venue=execution_venue,
|
1123
1527
|
odir=odir,
|
@@ -1128,7 +1532,7 @@ class AsyncClient:
|
|
1128
1532
|
time_in_force=time_in_force,
|
1129
1533
|
)
|
1130
1534
|
|
1131
|
-
async def cancel_order(self, order_id:
|
1535
|
+
async def cancel_order(self, order_id: OrderId) -> Cancel:
|
1132
1536
|
"""
|
1133
1537
|
Cancels an order by order id.
|
1134
1538
|
|
@@ -1137,10 +1541,17 @@ class AsyncClient:
|
|
1137
1541
|
Returns:
|
1138
1542
|
the CancelFields object
|
1139
1543
|
"""
|
1140
|
-
|
1141
|
-
|
1544
|
+
grpc_client = await self.core()
|
1545
|
+
req = CancelOrderRequest(id=order_id)
|
1546
|
+
res = await grpc_client.unary_unary(req)
|
1547
|
+
return res
|
1142
1548
|
|
1143
|
-
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:
|
1144
1555
|
"""
|
1145
1556
|
Cancels all open orders.
|
1146
1557
|
|
@@ -1148,5 +1559,25 @@ class AsyncClient:
|
|
1148
1559
|
True if all orders were cancelled successfully
|
1149
1560
|
False if there was an error
|
1150
1561
|
"""
|
1151
|
-
|
1152
|
-
|
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
|