architect-py 5.1.0b1__py3-none-any.whl → 5.1.2__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 +3 -1
- architect_py/async_client.py +116 -28
- architect_py/client.py +56 -70
- architect_py/client.pyi +54 -12
- architect_py/common_types/time_in_force.py +1 -0
- architect_py/common_types/tradable_product.py +1 -1
- architect_py/graphql_client/juniper_base_client.py +4 -0
- architect_py/grpc/client.py +3 -0
- architect_py/grpc/models/Cpty/CptyStatus.py +17 -18
- architect_py/grpc/models/Marketdata/Ticker.py +14 -1
- architect_py/grpc/models/Oms/Order.py +12 -1
- architect_py/grpc/models/definitions.py +41 -0
- architect_py/grpc/resolve_endpoint.py +11 -2
- architect_py/tests/conftest.py +2 -5
- architect_py/tests/test_encoding.py +37 -0
- architect_py/tests/test_marketdata.py +9 -0
- architect_py/tests/test_order_entry.py +3 -1
- architect_py/tests/test_portfolio_management.py +2 -0
- architect_py/tests/test_symbology.py +20 -0
- architect_py/tests/test_sync_client.py +40 -0
- {architect_py-5.1.0b1.dist-info → architect_py-5.1.2.dist-info}/METADATA +4 -1
- {architect_py-5.1.0b1.dist-info → architect_py-5.1.2.dist-info}/RECORD +35 -33
- {architect_py-5.1.0b1.dist-info → architect_py-5.1.2.dist-info}/WHEEL +1 -1
- examples/book_subscription.py +2 -0
- examples/candles.py +2 -0
- examples/funding_rate_mean_reversion_algo.py +1 -0
- examples/order_sending.py +5 -3
- examples/stream_l1_marketdata.py +2 -0
- examples/stream_l2_marketdata.py +2 -0
- examples/trades.py +2 -0
- examples/tutorial_async.py +3 -1
- examples/tutorial_sync.py +4 -1
- templates/juniper_base_client.py +4 -0
- {architect_py-5.1.0b1.dist-info → architect_py-5.1.2.dist-info}/licenses/LICENSE +0 -0
- {architect_py-5.1.0b1.dist-info → architect_py-5.1.2.dist-info}/top_level.txt +0 -0
architect_py/__init__.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# ruff: noqa:I001
|
2
2
|
|
3
|
-
__version__ = "5.1.
|
3
|
+
__version__ = "5.1.2"
|
4
4
|
|
5
5
|
from .utils.nearest_tick import TickRoundMethod
|
6
6
|
from .async_client import AsyncClient
|
@@ -13,6 +13,7 @@ from .grpc.models.definitions import (
|
|
13
13
|
AlgoOrderStatus,
|
14
14
|
CancelStatus,
|
15
15
|
CandleWidth,
|
16
|
+
ConnectionStatus,
|
16
17
|
CptyLogoutRequest,
|
17
18
|
DateTimeOrUtc,
|
18
19
|
Deposit,
|
@@ -269,6 +270,7 @@ __all__ = [
|
|
269
270
|
"Commodity",
|
270
271
|
"ConfigRequest",
|
271
272
|
"ConfigResponse",
|
273
|
+
"ConnectionStatus",
|
272
274
|
"CptyLoginRequest",
|
273
275
|
"CptyLogoutRequest",
|
274
276
|
"CptyRequest",
|
architect_py/async_client.py
CHANGED
@@ -43,7 +43,7 @@ from architect_py.grpc.models.Orderflow.OrderflowRequest import (
|
|
43
43
|
OrderflowRequest_route,
|
44
44
|
OrderflowRequestUnannotatedResponseType,
|
45
45
|
)
|
46
|
-
from architect_py.grpc.resolve_endpoint import resolve_endpoint
|
46
|
+
from architect_py.grpc.resolve_endpoint import PAPER_GRPC_PORT, resolve_endpoint
|
47
47
|
from architect_py.utils.nearest_tick import TickRoundMethod
|
48
48
|
from architect_py.utils.orderbook import update_orderbook_side
|
49
49
|
from architect_py.utils.pandas import candles_to_dataframe
|
@@ -102,7 +102,9 @@ class AsyncClient:
|
|
102
102
|
)
|
103
103
|
endpoint = kwargs["endpoint"]
|
104
104
|
|
105
|
-
grpc_host, grpc_port, use_ssl = await resolve_endpoint(
|
105
|
+
grpc_host, grpc_port, use_ssl = await resolve_endpoint(
|
106
|
+
endpoint, paper_trading=paper_trading
|
107
|
+
)
|
106
108
|
logging.info(
|
107
109
|
f"Resolved endpoint {endpoint}: {grpc_host}:{grpc_port} use_ssl={use_ssl}"
|
108
110
|
)
|
@@ -110,7 +112,7 @@ class AsyncClient:
|
|
110
112
|
# Sanity check paper trading on prod environments
|
111
113
|
if paper_trading:
|
112
114
|
if grpc_host == "app.architect.co" or grpc_host == "staging.architect.co":
|
113
|
-
if grpc_port !=
|
115
|
+
if grpc_port != PAPER_GRPC_PORT:
|
114
116
|
raise ValueError("Wrong gRPC port for paper trading")
|
115
117
|
if graphql_port is not None and graphql_port != 5678:
|
116
118
|
raise ValueError("Wrong GraphQL port for paper trading")
|
@@ -179,6 +181,29 @@ class AsyncClient:
|
|
179
181
|
)
|
180
182
|
self.grpc_core = GrpcClient(host=grpc_host, port=grpc_port, use_ssl=use_ssl)
|
181
183
|
|
184
|
+
async def close(self):
|
185
|
+
"""
|
186
|
+
Close the gRPC channel and GraphQL client.
|
187
|
+
|
188
|
+
This fixes the:
|
189
|
+
Error in sys.excepthook:
|
190
|
+
|
191
|
+
Original exception was:
|
192
|
+
|
193
|
+
One might get when closing the client
|
194
|
+
"""
|
195
|
+
if self.grpc_core is not None:
|
196
|
+
await self.grpc_core.close()
|
197
|
+
|
198
|
+
for grpc_client in self.grpc_marketdata.values():
|
199
|
+
await grpc_client.close()
|
200
|
+
|
201
|
+
self.grpc_marketdata.clear()
|
202
|
+
# NB: this line removes the "Error in sys.excepthook:" on close
|
203
|
+
|
204
|
+
if self.graphql_client is not None:
|
205
|
+
await self.graphql_client.close()
|
206
|
+
|
182
207
|
async def refresh_jwt(self, force: bool = False):
|
183
208
|
"""
|
184
209
|
Refresh the JWT for the gRPC channel if it's nearing expiration (within 1 minute).
|
@@ -259,6 +284,9 @@ class AsyncClient:
|
|
259
284
|
self.grpc_marketdata[venue] = GrpcClient(
|
260
285
|
host=grpc_host, port=grpc_port, use_ssl=use_ssl
|
261
286
|
)
|
287
|
+
logging.debug(
|
288
|
+
f"Setting marketdata endpoint for {venue}: {grpc_host}:{grpc_port} use_ssl={use_ssl}"
|
289
|
+
)
|
262
290
|
except Exception as e:
|
263
291
|
logging.error("Failed to set marketdata endpoint: %s", e)
|
264
292
|
|
@@ -503,34 +531,51 @@ class AsyncClient:
|
|
503
531
|
if not series_symbol.endswith("Futures"):
|
504
532
|
raise ValueError("series_symbol must end with 'Futures'")
|
505
533
|
res = await self.graphql_client.get_future_series_query(series_symbol)
|
506
|
-
|
534
|
+
|
535
|
+
today = date.today()
|
536
|
+
|
537
|
+
futures = [
|
538
|
+
future
|
539
|
+
for future in res.futures_series
|
540
|
+
if (exp := nominative_expiration(future)) is not None and exp > today
|
541
|
+
]
|
542
|
+
futures.sort()
|
543
|
+
|
544
|
+
return futures
|
507
545
|
|
508
546
|
async def get_front_future(
|
509
|
-
self, series_symbol: str, venue: str
|
510
|
-
) ->
|
547
|
+
self, series_symbol: str, venue: Optional[str] = None
|
548
|
+
) -> TradableProduct:
|
511
549
|
"""
|
512
|
-
Gets the future
|
550
|
+
Gets the front future.
|
551
|
+
** If the venue is provided, it will return the future with the most volume in that venue**
|
552
|
+
Otherwise, will sort by expiration date and return the earliest future.
|
553
|
+
|
554
|
+
** Note that this function returns a TradableProduct (ie with a base and a quote)
|
555
|
+
|
513
556
|
|
514
557
|
Args:
|
515
558
|
series_symbol: the futures series
|
516
559
|
e.g. "ES CME Futures" would yield the lead future for the ES series
|
517
560
|
venue: the venue to get the lead future for, e.g. "CME"
|
518
|
-
|
561
|
+
** If the venue is provided, it will return the future with the most volume in that venue**
|
519
562
|
|
520
563
|
Returns:
|
521
564
|
The lead future symbol
|
522
565
|
"""
|
523
566
|
futures = await self.get_futures_series(series_symbol)
|
524
|
-
if not
|
567
|
+
if not venue:
|
525
568
|
futures.sort()
|
526
|
-
return futures[0]
|
569
|
+
return TradableProduct(futures[0], "USD")
|
527
570
|
else:
|
528
571
|
grpc_client = await self.marketdata(venue)
|
529
572
|
req = TickersRequest(
|
530
|
-
symbols=futures,
|
573
|
+
symbols=[TradableProduct(f"{future}/USD") for future in futures],
|
574
|
+
k=SortTickersBy.VOLUME_DESC,
|
575
|
+
venue=venue,
|
531
576
|
)
|
532
577
|
res: TickersResponse = await grpc_client.unary_unary(req)
|
533
|
-
return res.tickers[0].symbol
|
578
|
+
return TradableProduct(res.tickers[0].symbol)
|
534
579
|
|
535
580
|
@staticmethod
|
536
581
|
def get_expiration_from_CME_name(name: str) -> Optional[date]:
|
@@ -1337,8 +1382,7 @@ class AsyncClient:
|
|
1337
1382
|
|
1338
1383
|
Example:
|
1339
1384
|
```python
|
1340
|
-
|
1341
|
-
async for of in client.subscribe_orderflow_stream(request):
|
1385
|
+
async for of in client.stream_orderflow(account, execution_venue, trader):
|
1342
1386
|
print(of)
|
1343
1387
|
```
|
1344
1388
|
"""
|
@@ -1375,6 +1419,48 @@ class AsyncClient:
|
|
1375
1419
|
"""
|
1376
1420
|
return await self.place_limit_order(*args, **kwargs)
|
1377
1421
|
|
1422
|
+
async def place_orders(
|
1423
|
+
self, order_requests: Sequence[PlaceOrderRequest]
|
1424
|
+
) -> list[Order]:
|
1425
|
+
"""
|
1426
|
+
A low level function to place multiple orders in a single function.
|
1427
|
+
|
1428
|
+
This function does NOT check the validity of the parameters, so it is the user's responsibility
|
1429
|
+
to ensure that the orders are valid and will not be rejected by the OMS.
|
1430
|
+
|
1431
|
+
Args:
|
1432
|
+
order_request: the PlaceOrderRequest containing the orders to place
|
1433
|
+
|
1434
|
+
|
1435
|
+
Example of a PlaceOrderRequest:
|
1436
|
+
order_request: PlaceOrderRequest = PlaceOrderRequest.new(
|
1437
|
+
dir=dir,
|
1438
|
+
quantity=quantity,
|
1439
|
+
symbol=symbol,
|
1440
|
+
time_in_force=time_in_force,
|
1441
|
+
limit_price=limit_price,
|
1442
|
+
order_type=order_type,
|
1443
|
+
account=account,
|
1444
|
+
id=id,
|
1445
|
+
parent_id=None,
|
1446
|
+
source=OrderSource.API,
|
1447
|
+
trader=trader,
|
1448
|
+
execution_venue=execution_venue,
|
1449
|
+
post_only=post_only,
|
1450
|
+
trigger_price=trigger_price,
|
1451
|
+
)
|
1452
|
+
"""
|
1453
|
+
grpc_client = await self.core()
|
1454
|
+
|
1455
|
+
res = await asyncio.gather(
|
1456
|
+
*[
|
1457
|
+
grpc_client.unary_unary(order_request)
|
1458
|
+
for order_request in order_requests
|
1459
|
+
]
|
1460
|
+
)
|
1461
|
+
|
1462
|
+
return res
|
1463
|
+
|
1378
1464
|
async def place_limit_order(
|
1379
1465
|
self,
|
1380
1466
|
*,
|
@@ -1448,7 +1534,7 @@ class AsyncClient:
|
|
1448
1534
|
)
|
1449
1535
|
if execution_info is None:
|
1450
1536
|
raise ValueError(
|
1451
|
-
"Could not find execution information for {symbol} for rounding price for limit order. Please round price manually."
|
1537
|
+
f"Could not find execution information for {symbol} for rounding price for limit order. Please round price manually."
|
1452
1538
|
)
|
1453
1539
|
if (tick_size := execution_info.tick_size) is not None:
|
1454
1540
|
if tick_size:
|
@@ -1481,7 +1567,7 @@ class AsyncClient:
|
|
1481
1567
|
id: Optional[OrderId] = None,
|
1482
1568
|
symbol: TradableProduct | str,
|
1483
1569
|
execution_venue: str,
|
1484
|
-
|
1570
|
+
dir: OrderDir,
|
1485
1571
|
quantity: Decimal,
|
1486
1572
|
time_in_force: TimeInForce = TimeInForce.DAY,
|
1487
1573
|
account: Optional[str] = None,
|
@@ -1495,7 +1581,7 @@ class AsyncClient:
|
|
1495
1581
|
id: in case user wants to generate their own order id, otherwise it will be generated automatically
|
1496
1582
|
symbol: the symbol to send the order for
|
1497
1583
|
execution_venue: the execution venue to send the order to
|
1498
|
-
|
1584
|
+
dir: the direction of the order
|
1499
1585
|
quantity: the quantity of the order
|
1500
1586
|
time_in_force: the time in force of the order
|
1501
1587
|
account: the account to send the order for
|
@@ -1516,7 +1602,7 @@ class AsyncClient:
|
|
1516
1602
|
|
1517
1603
|
price_band = price_band_pairs.get(symbol, None)
|
1518
1604
|
|
1519
|
-
if
|
1605
|
+
if dir == OrderDir.BUY:
|
1520
1606
|
if ticker.ask_price is None:
|
1521
1607
|
raise ValueError(
|
1522
1608
|
f"Failed to send market order with reason: no ask price for {symbol}"
|
@@ -1539,7 +1625,7 @@ class AsyncClient:
|
|
1539
1625
|
|
1540
1626
|
# Conservatively round price to nearest tick
|
1541
1627
|
tick_round_method = (
|
1542
|
-
TickRoundMethod.FLOOR if
|
1628
|
+
TickRoundMethod.FLOOR if dir == OrderDir.BUY else TickRoundMethod.CEIL
|
1543
1629
|
)
|
1544
1630
|
|
1545
1631
|
execution_info = await self.get_execution_info(
|
@@ -1556,7 +1642,7 @@ class AsyncClient:
|
|
1556
1642
|
id=id,
|
1557
1643
|
symbol=symbol,
|
1558
1644
|
execution_venue=execution_venue,
|
1559
|
-
|
1645
|
+
dir=dir,
|
1560
1646
|
quantity=quantity,
|
1561
1647
|
account=account,
|
1562
1648
|
order_type=OrderType.LIMIT,
|
@@ -1605,11 +1691,13 @@ class AsyncClient:
|
|
1605
1691
|
if cancel.reject_reason is not None:
|
1606
1692
|
return False
|
1607
1693
|
return True
|
1608
|
-
grpc_client = await self.core()
|
1609
|
-
|
1610
|
-
|
1611
|
-
|
1612
|
-
|
1613
|
-
|
1614
|
-
|
1615
|
-
|
1694
|
+
# grpc_client = await self.core()
|
1695
|
+
|
1696
|
+
# req = CancelAllOrdersRequest(
|
1697
|
+
# id=str(uuid.uuid4()), # Unique ID for the request
|
1698
|
+
# account=account,
|
1699
|
+
# execution_venue=execution_venue,
|
1700
|
+
# trader=trader,
|
1701
|
+
# )
|
1702
|
+
# res = await grpc_client.unary_unary(req)
|
1703
|
+
# return True
|
architect_py/client.py
CHANGED
@@ -2,13 +2,12 @@ import asyncio
|
|
2
2
|
import sys
|
3
3
|
import threading
|
4
4
|
from asyncio import AbstractEventLoop
|
5
|
+
from collections.abc import Callable
|
5
6
|
from functools import partial
|
6
|
-
from typing import Any,
|
7
|
+
from typing import Any, Concatenate, Coroutine, Optional, ParamSpec, TypeVar
|
7
8
|
|
8
9
|
from .async_client import AsyncClient
|
9
10
|
|
10
|
-
T = TypeVar("T")
|
11
|
-
|
12
11
|
|
13
12
|
def is_async_function(obj):
|
14
13
|
# can be converted to C function for faster performance
|
@@ -16,24 +15,30 @@ def is_async_function(obj):
|
|
16
15
|
return callable(obj) and hasattr(obj, "__code__") and obj.__code__.co_flags & 0x80
|
17
16
|
|
18
17
|
|
18
|
+
P = ParamSpec("P")
|
19
|
+
T = TypeVar("T")
|
20
|
+
|
21
|
+
|
19
22
|
class Client:
|
20
23
|
"""
|
24
|
+
One can find the function definition in the AsyncClient class and in the pyi file.
|
25
|
+
|
21
26
|
This class is a wrapper around the AsyncClient class that allows you to call async methods synchronously.
|
22
27
|
This does not work for subscription based methods.
|
23
28
|
|
24
29
|
This Client takes control of the event loop, which you can pass in.
|
25
30
|
|
26
|
-
One can find the function definition in the AsyncClient class.
|
27
31
|
|
28
32
|
The AsyncClient is more performant and powerful, so it is recommended to use that class if possible.
|
29
|
-
|
30
|
-
Avoid adding functions or other attributes to this class unless you know what you are doing, because
|
31
|
-
the __getattribute__ method changes the behavior of the class in a way that is not intuitive.
|
32
|
-
|
33
|
-
Instead, add them to the AsyncClient class.
|
33
|
+
Avoid adding functions or other attributes to this class unless you know what you are doing.
|
34
34
|
"""
|
35
35
|
|
36
|
-
__slots__ = (
|
36
|
+
__slots__ = (
|
37
|
+
"client",
|
38
|
+
"_event_loop",
|
39
|
+
"_sync_call",
|
40
|
+
"__dict__",
|
41
|
+
)
|
37
42
|
client: AsyncClient
|
38
43
|
_event_loop: AbstractEventLoop
|
39
44
|
|
@@ -56,88 +61,69 @@ class Client:
|
|
56
61
|
Pass in an `event_loop` if you want to use your own; otherwise, this class
|
57
62
|
will use the default asyncio event loop.
|
58
63
|
"""
|
64
|
+
self._sync_call = self._pick_executor()
|
65
|
+
|
59
66
|
if event_loop is None:
|
60
67
|
try:
|
61
68
|
event_loop = asyncio.get_running_loop()
|
62
69
|
except RuntimeError:
|
63
70
|
event_loop = asyncio.new_event_loop()
|
64
71
|
asyncio.set_event_loop(event_loop)
|
65
|
-
|
66
|
-
|
67
|
-
async_client = self.
|
68
|
-
AsyncClient.connect
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
)
|
72
|
+
object.__setattr__(self, "_event_loop", event_loop)
|
73
|
+
|
74
|
+
async_client: AsyncClient = self._sync_call(
|
75
|
+
AsyncClient.connect,
|
76
|
+
api_key=api_key,
|
77
|
+
api_secret=api_secret,
|
78
|
+
paper_trading=paper_trading,
|
79
|
+
endpoint=endpoint,
|
80
|
+
graphql_port=graphql_port,
|
81
|
+
**kwargs,
|
76
82
|
)
|
77
|
-
|
83
|
+
|
84
|
+
object.__setattr__(
|
85
|
+
self,
|
78
86
|
"client",
|
79
87
|
async_client,
|
80
88
|
)
|
89
|
+
self._promote_async_client_methods()
|
81
90
|
|
91
|
+
def _pick_executor(
|
92
|
+
self,
|
93
|
+
) -> Callable[
|
94
|
+
Concatenate[Callable[P, Coroutine[Any, Any, T]], P],
|
95
|
+
T,
|
96
|
+
]:
|
97
|
+
"""Return a function that runs a coroutine and blocks."""
|
82
98
|
if "ipykernel" in sys.modules:
|
83
|
-
|
99
|
+
executor = AsyncExecutor()
|
84
100
|
import atexit
|
85
101
|
|
86
|
-
executor = AsyncExecutor()
|
87
102
|
atexit.register(executor.shutdown)
|
103
|
+
return lambda fn, *a, **kw: executor.submit(fn(*a, **kw))
|
88
104
|
|
89
|
-
|
90
|
-
async_method: Callable[..., Coroutine[Any, Any, T]],
|
91
|
-
*args,
|
92
|
-
**kwargs,
|
93
|
-
) -> T:
|
94
|
-
"""
|
95
|
-
Executes the given coroutine synchronously using the executor.
|
96
|
-
"""
|
97
|
-
return executor.submit(async_method(*args, **kwargs))
|
105
|
+
return lambda fn, *a, **kw: self._event_loop.run_until_complete(fn(*a, **kw))
|
98
106
|
|
99
|
-
|
107
|
+
def _promote_async_client_methods(self) -> None:
|
108
|
+
for name in dir(self.client):
|
109
|
+
if name.startswith("_"):
|
110
|
+
continue
|
100
111
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
It can be found if you look in the AsyncClient class, which this class is a wrapper for,
|
105
|
-
or GraphQLClient, which is a parent class of AsyncClient
|
112
|
+
if any(x in name for x in ("stream", "subscribe", "connect")):
|
113
|
+
continue
|
114
|
+
attr = getattr(self.client, name)
|
106
115
|
|
107
|
-
|
108
|
-
|
109
|
-
In this case, will look through self.client, which is an instance of the Client class
|
116
|
+
if is_async_function(attr):
|
117
|
+
attr = partial(self._sync_call, attr)
|
110
118
|
|
111
|
-
|
112
|
-
but otherwise pass through the other attributes normally
|
119
|
+
object.__setattr__(self, name, attr)
|
113
120
|
|
114
|
-
|
115
|
-
|
116
|
-
in
|
117
|
-
|
118
|
-
attr = getattr(super().__getattribute__("client"), name)
|
119
|
-
if is_async_function(attr):
|
120
|
-
if "subscribe" in name:
|
121
|
-
raise AttributeError(
|
122
|
-
f"Method {name} is an subscription based async method and cannot be called synchronously"
|
123
|
-
)
|
124
|
-
return partial(super().__getattribute__("_sync_call"), attr)
|
121
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
122
|
+
# protect wrapper internals
|
123
|
+
if name in ("client", "_event_loop", "_sync_call"):
|
124
|
+
object.__setattr__(self, name, value)
|
125
125
|
else:
|
126
|
-
|
127
|
-
|
128
|
-
def __setattr__(self, name: str, value: Any):
|
129
|
-
"""primarily to prevent unintended shadowing"""
|
130
|
-
client = super().__getattribute__("client")
|
131
|
-
setattr(client, name, value)
|
132
|
-
|
133
|
-
def _sync_call(
|
134
|
-
self, async_method: Callable[..., Awaitable[T]], *args, **kwargs
|
135
|
-
) -> T:
|
136
|
-
return (
|
137
|
-
super()
|
138
|
-
.__getattribute__("_event_loop")
|
139
|
-
.run_until_complete(async_method(*args, **kwargs))
|
140
|
-
)
|
126
|
+
setattr(self.client, name, value)
|
141
127
|
|
142
128
|
|
143
129
|
class AsyncExecutor:
|
architect_py/client.pyi
CHANGED
@@ -11,7 +11,7 @@ from architect_py.graphql_client.fragments import ExecutionInfoFields as Executi
|
|
11
11
|
from architect_py.grpc.client import GrpcClient as GrpcClient
|
12
12
|
from architect_py.grpc.models.Orderflow.OrderflowRequest import OrderflowRequestUnannotatedResponseType as OrderflowRequestUnannotatedResponseType, OrderflowRequest_route as OrderflowRequest_route
|
13
13
|
from architect_py.grpc.models.definitions import AccountIdOrName as AccountIdOrName, AccountWithPermissions as AccountWithPermissions, CandleWidth as CandleWidth, L2BookDiff as L2BookDiff, OrderId as OrderId, OrderSource as OrderSource, OrderType as OrderType, SortTickersBy as SortTickersBy, TraderIdOrEmail as TraderIdOrEmail
|
14
|
-
from architect_py.grpc.resolve_endpoint import resolve_endpoint as resolve_endpoint
|
14
|
+
from architect_py.grpc.resolve_endpoint import PAPER_GRPC_PORT as PAPER_GRPC_PORT, resolve_endpoint as resolve_endpoint
|
15
15
|
from architect_py.utils.nearest_tick import TickRoundMethod as TickRoundMethod
|
16
16
|
from architect_py.utils.orderbook import update_orderbook_side as update_orderbook_side
|
17
17
|
from architect_py.utils.pandas import candles_to_dataframe as candles_to_dataframe
|
@@ -23,19 +23,16 @@ from typing import Any, AsyncGenerator, AsyncIterator, Literal, Sequence, overlo
|
|
23
23
|
|
24
24
|
class Client:
|
25
25
|
"""
|
26
|
+
One can find the function definition in the AsyncClient class and in the pyi file.
|
27
|
+
|
26
28
|
This class is a wrapper around the AsyncClient class that allows you to call async methods synchronously.
|
27
29
|
This does not work for subscription based methods.
|
28
30
|
|
29
31
|
This Client takes control of the event loop, which you can pass in.
|
30
32
|
|
31
|
-
One can find the function definition in the AsyncClient class.
|
32
33
|
|
33
34
|
The AsyncClient is more performant and powerful, so it is recommended to use that class if possible.
|
34
|
-
|
35
|
-
Avoid adding functions or other attributes to this class unless you know what you are doing, because
|
36
|
-
the __getattribute__ method changes the behavior of the class in a way that is not intuitive.
|
37
|
-
|
38
|
-
Instead, add them to the AsyncClient class.
|
35
|
+
Avoid adding functions or other attributes to this class unless you know what you are doing.
|
39
36
|
"""
|
40
37
|
api_key: str | None
|
41
38
|
api_secret: str | None
|
@@ -57,6 +54,17 @@ class Client:
|
|
57
54
|
will use the default asyncio event loop.
|
58
55
|
"""
|
59
56
|
l2_books: dict[Venue, dict[TradableProduct, tuple[L2BookSnapshot, asyncio.Task]]]
|
57
|
+
def close(self) -> None:
|
58
|
+
"""
|
59
|
+
Close the gRPC channel and GraphQL client.
|
60
|
+
|
61
|
+
This fixes the:
|
62
|
+
Error in sys.excepthook:
|
63
|
+
|
64
|
+
Original exception was:
|
65
|
+
|
66
|
+
One might get when closing the client
|
67
|
+
"""
|
60
68
|
def refresh_jwt(self, force: bool = False):
|
61
69
|
"""
|
62
70
|
Refresh the JWT for the gRPC channel if it's nearing expiration (within 1 minute).
|
@@ -209,15 +217,20 @@ class Client:
|
|
209
217
|
Returns:
|
210
218
|
List of futures products
|
211
219
|
'''
|
212
|
-
def get_front_future(self, series_symbol: str, venue: str
|
220
|
+
def get_front_future(self, series_symbol: str, venue: str | None = None) -> TradableProduct:
|
213
221
|
'''
|
214
|
-
Gets the future
|
222
|
+
Gets the front future.
|
223
|
+
** If the venue is provided, it will return the future with the most volume in that venue**
|
224
|
+
Otherwise, will sort by expiration date and return the earliest future.
|
225
|
+
|
226
|
+
** Note that this function returns a TradableProduct (ie with a base and a quote)
|
227
|
+
|
215
228
|
|
216
229
|
Args:
|
217
230
|
series_symbol: the futures series
|
218
231
|
e.g. "ES CME Futures" would yield the lead future for the ES series
|
219
232
|
venue: the venue to get the lead future for, e.g. "CME"
|
220
|
-
|
233
|
+
** If the venue is provided, it will return the future with the most volume in that venue**
|
221
234
|
|
222
235
|
Returns:
|
223
236
|
The lead future symbol
|
@@ -435,6 +448,35 @@ class Client:
|
|
435
448
|
'''
|
436
449
|
@deprecated(reason="Use place_limit_order instead")
|
437
450
|
'''
|
451
|
+
def place_orders(self, order_requests: Sequence[PlaceOrderRequest]) -> list[Order]:
|
452
|
+
"""
|
453
|
+
A low level function to place multiple orders in a single function.
|
454
|
+
|
455
|
+
This function does NOT check the validity of the parameters, so it is the user's responsibility
|
456
|
+
to ensure that the orders are valid and will not be rejected by the OMS.
|
457
|
+
|
458
|
+
Args:
|
459
|
+
order_request: the PlaceOrderRequest containing the orders to place
|
460
|
+
|
461
|
+
|
462
|
+
Example of a PlaceOrderRequest:
|
463
|
+
order_request: PlaceOrderRequest = PlaceOrderRequest.new(
|
464
|
+
dir=dir,
|
465
|
+
quantity=quantity,
|
466
|
+
symbol=symbol,
|
467
|
+
time_in_force=time_in_force,
|
468
|
+
limit_price=limit_price,
|
469
|
+
order_type=order_type,
|
470
|
+
account=account,
|
471
|
+
id=id,
|
472
|
+
parent_id=None,
|
473
|
+
source=OrderSource.API,
|
474
|
+
trader=trader,
|
475
|
+
execution_venue=execution_venue,
|
476
|
+
post_only=post_only,
|
477
|
+
trigger_price=trigger_price,
|
478
|
+
)
|
479
|
+
"""
|
438
480
|
def place_limit_order(self, *, id: OrderId | None = None, symbol: TradableProduct | str, execution_venue: str | None = None, dir: OrderDir | None = None, quantity: Decimal, limit_price: Decimal, order_type: OrderType = ..., time_in_force: TimeInForce = ..., price_round_method: TickRoundMethod | None = None, account: str | None = None, trader: str | None = None, post_only: bool = False, trigger_price: Decimal | None = None, **kwargs: Any) -> Order:
|
439
481
|
'''
|
440
482
|
Sends a regular limit order.
|
@@ -464,7 +506,7 @@ class Client:
|
|
464
506
|
|
465
507
|
If the order is rejected, the order.reject_reason and order.reject_message will be set
|
466
508
|
'''
|
467
|
-
def send_market_pro_order(self, *, id: OrderId | None = None, symbol: TradableProduct | str, execution_venue: str,
|
509
|
+
def send_market_pro_order(self, *, id: OrderId | None = None, symbol: TradableProduct | str, execution_venue: str, dir: OrderDir, quantity: Decimal, time_in_force: TimeInForce = ..., account: str | None = None, fraction_through_market: Decimal = ...) -> Order:
|
468
510
|
'''
|
469
511
|
Sends a market-order like limit price based on the BBO.
|
470
512
|
Meant to behave as a market order but with more protections.
|
@@ -473,7 +515,7 @@ class Client:
|
|
473
515
|
id: in case user wants to generate their own order id, otherwise it will be generated automatically
|
474
516
|
symbol: the symbol to send the order for
|
475
517
|
execution_venue: the execution venue to send the order to
|
476
|
-
|
518
|
+
dir: the direction of the order
|
477
519
|
quantity: the quantity of the order
|
478
520
|
time_in_force: the time in force of the order
|
479
521
|
account: the account to send the order for
|
@@ -54,7 +54,7 @@ class TradableProduct(str):
|
|
54
54
|
return self.split("/", 1)[1]
|
55
55
|
|
56
56
|
def serialize(self) -> msgspec.Raw:
|
57
|
-
return msgspec.Raw(
|
57
|
+
return msgspec.Raw(msgspec.json.encode(str(self)))
|
58
58
|
|
59
59
|
@staticmethod
|
60
60
|
def deserialize(s: str) -> "TradableProduct":
|