architect-py 5.1.3__py3-none-any.whl → 5.1.4__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.
Files changed (32) hide show
  1. architect_py/__init__.py +19 -1
  2. architect_py/async_client.py +136 -49
  3. architect_py/client.py +15 -1
  4. architect_py/client.pyi +26 -8
  5. architect_py/grpc/client.py +37 -13
  6. architect_py/grpc/models/AlgoHelper/AlgoParamTypes.py +31 -0
  7. architect_py/grpc/models/AlgoHelper/__init__.py +2 -0
  8. architect_py/grpc/models/Auth/AuthInfoRequest.py +37 -0
  9. architect_py/grpc/models/Auth/AuthInfoResponse.py +30 -0
  10. architect_py/grpc/models/Folio/AccountHistoryRequest.py +35 -4
  11. architect_py/grpc/models/Marketdata/Candle.py +6 -0
  12. architect_py/grpc/models/Marketdata/L1BookSnapshot.py +17 -3
  13. architect_py/grpc/models/Marketdata/L2BookSnapshot.py +6 -0
  14. architect_py/grpc/models/Marketdata/Liquidation.py +6 -0
  15. architect_py/grpc/models/Marketdata/Ticker.py +6 -0
  16. architect_py/grpc/models/Marketdata/TickersRequest.py +4 -1
  17. architect_py/grpc/models/Marketdata/Trade.py +6 -0
  18. architect_py/grpc/models/Oms/Order.py +38 -14
  19. architect_py/grpc/models/Oms/PlaceOrderRequest.py +38 -14
  20. architect_py/grpc/models/__init__.py +4 -1
  21. architect_py/grpc/models/definitions.py +174 -0
  22. architect_py/grpc/orderflow.py +138 -0
  23. architect_py/tests/test_order_entry.py +9 -6
  24. architect_py/tests/test_orderflow.py +116 -27
  25. {architect_py-5.1.3.dist-info → architect_py-5.1.4.dist-info}/METADATA +1 -1
  26. {architect_py-5.1.3.dist-info → architect_py-5.1.4.dist-info}/RECORD +32 -27
  27. examples/funding_rate_mean_reversion_algo.py +23 -47
  28. scripts/correct_sync_interface.py +5 -2
  29. scripts/postprocess_grpc.py +17 -2
  30. {architect_py-5.1.3.dist-info → architect_py-5.1.4.dist-info}/WHEEL +0 -0
  31. {architect_py-5.1.3.dist-info → architect_py-5.1.4.dist-info}/licenses/LICENSE +0 -0
  32. {architect_py-5.1.3.dist-info → architect_py-5.1.4.dist-info}/top_level.txt +0 -0
@@ -11,6 +11,9 @@ from .Algo.StartAlgoRequest import StartAlgoRequest
11
11
  from .Algo.StartAlgoResponse import StartAlgoResponse
12
12
  from .Algo.StopAlgoRequest import StopAlgoRequest
13
13
  from .Algo.StopAlgoResponse import StopAlgoResponse
14
+ from .AlgoHelper.AlgoParamTypes import AlgoParamTypes
15
+ from .Auth.AuthInfoRequest import AuthInfoRequest
16
+ from .Auth.AuthInfoResponse import AuthInfoResponse
14
17
  from .Auth.CreateJwtRequest import CreateJwtRequest
15
18
  from .Auth.CreateJwtResponse import CreateJwtResponse
16
19
  from .Boss.DepositsRequest import DepositsRequest
@@ -110,4 +113,4 @@ from .Symbology.UploadProductCatalogResponse import UploadProductCatalogResponse
110
113
  from .Symbology.UploadSymbologyRequest import UploadSymbologyRequest
111
114
  from .Symbology.UploadSymbologyResponse import UploadSymbologyResponse
112
115
 
113
- __all__ = ["AccountsRequest", "AccountsResponse", "AlgoOrder", "AlgoOrderRequest", "AlgoOrdersRequest", "AlgoOrdersResponse", "CreateAlgoOrderRequest", "PauseAlgoRequest", "PauseAlgoResponse", "StartAlgoRequest", "StartAlgoResponse", "StopAlgoRequest", "StopAlgoResponse", "CreateJwtRequest", "CreateJwtResponse", "DepositsRequest", "DepositsResponse", "RqdAccountStatisticsRequest", "RqdAccountStatisticsResponse", "StatementUrlRequest", "StatementUrlResponse", "StatementsRequest", "StatementsResponse", "WithdrawalsRequest", "WithdrawalsResponse", "ConfigRequest", "ConfigResponse", "RestartCptyRequest", "RestartCptyResponse", "CptyRequest", "CptyResponse", "CptyStatus", "CptyStatusRequest", "CptysRequest", "CptysResponse", "AccountHistoryRequest", "AccountHistoryResponse", "AccountSummariesRequest", "AccountSummariesResponse", "AccountSummary", "AccountSummaryRequest", "HistoricalFillsRequest", "HistoricalFillsResponse", "HistoricalOrdersRequest", "HistoricalOrdersResponse", "HealthCheckRequest", "HealthCheckResponse", "ArrayOfL1BookSnapshot", "Candle", "HistoricalCandlesRequest", "HistoricalCandlesResponse", "L1BookSnapshot", "L1BookSnapshotRequest", "L1BookSnapshotsRequest", "L2BookSnapshot", "L2BookSnapshotRequest", "L2BookUpdate", "Liquidation", "MarketStatus", "MarketStatusRequest", "SubscribeCandlesRequest", "SubscribeCurrentCandlesRequest", "SubscribeL1BookSnapshotsRequest", "SubscribeL2BookUpdatesRequest", "SubscribeLiquidationsRequest", "SubscribeManyCandlesRequest", "SubscribeTickersRequest", "SubscribeTradesRequest", "Ticker", "TickerRequest", "TickerUpdate", "TickersRequest", "TickersResponse", "Trade", "Cancel", "CancelAllOrdersRequest", "CancelAllOrdersResponse", "CancelOrderRequest", "OpenOrdersRequest", "OpenOrdersResponse", "Order", "PendingCancelsRequest", "PendingCancelsResponse", "PlaceOrderRequest", "OptionsChain", "OptionsChainGreeks", "OptionsChainGreeksRequest", "OptionsChainRequest", "OptionsExpirations", "OptionsExpirationsRequest", "Dropcopy", "DropcopyRequest", "Orderflow", "OrderflowRequest", "SubscribeOrderflowRequest", "DownloadProductCatalogRequest", "DownloadProductCatalogResponse", "ExecutionInfoRequest", "ExecutionInfoResponse", "PruneExpiredSymbolsRequest", "PruneExpiredSymbolsResponse", "SubscribeSymbology", "SymbologyRequest", "SymbologySnapshot", "SymbologyUpdate", "SymbolsRequest", "SymbolsResponse", "UploadProductCatalogRequest", "UploadProductCatalogResponse", "UploadSymbologyRequest", "UploadSymbologyResponse"]
116
+ __all__ = ["AccountsRequest", "AccountsResponse", "AlgoOrder", "AlgoOrderRequest", "AlgoOrdersRequest", "AlgoOrdersResponse", "CreateAlgoOrderRequest", "PauseAlgoRequest", "PauseAlgoResponse", "StartAlgoRequest", "StartAlgoResponse", "StopAlgoRequest", "StopAlgoResponse", "AlgoParamTypes", "AuthInfoRequest", "AuthInfoResponse", "CreateJwtRequest", "CreateJwtResponse", "DepositsRequest", "DepositsResponse", "RqdAccountStatisticsRequest", "RqdAccountStatisticsResponse", "StatementUrlRequest", "StatementUrlResponse", "StatementsRequest", "StatementsResponse", "WithdrawalsRequest", "WithdrawalsResponse", "ConfigRequest", "ConfigResponse", "RestartCptyRequest", "RestartCptyResponse", "CptyRequest", "CptyResponse", "CptyStatus", "CptyStatusRequest", "CptysRequest", "CptysResponse", "AccountHistoryRequest", "AccountHistoryResponse", "AccountSummariesRequest", "AccountSummariesResponse", "AccountSummary", "AccountSummaryRequest", "HistoricalFillsRequest", "HistoricalFillsResponse", "HistoricalOrdersRequest", "HistoricalOrdersResponse", "HealthCheckRequest", "HealthCheckResponse", "ArrayOfL1BookSnapshot", "Candle", "HistoricalCandlesRequest", "HistoricalCandlesResponse", "L1BookSnapshot", "L1BookSnapshotRequest", "L1BookSnapshotsRequest", "L2BookSnapshot", "L2BookSnapshotRequest", "L2BookUpdate", "Liquidation", "MarketStatus", "MarketStatusRequest", "SubscribeCandlesRequest", "SubscribeCurrentCandlesRequest", "SubscribeL1BookSnapshotsRequest", "SubscribeL2BookUpdatesRequest", "SubscribeLiquidationsRequest", "SubscribeManyCandlesRequest", "SubscribeTickersRequest", "SubscribeTradesRequest", "Ticker", "TickerRequest", "TickerUpdate", "TickersRequest", "TickersResponse", "Trade", "Cancel", "CancelAllOrdersRequest", "CancelAllOrdersResponse", "CancelOrderRequest", "OpenOrdersRequest", "OpenOrdersResponse", "Order", "PendingCancelsRequest", "PendingCancelsResponse", "PlaceOrderRequest", "OptionsChain", "OptionsChainGreeks", "OptionsChainGreeksRequest", "OptionsChainRequest", "OptionsExpirations", "OptionsExpirationsRequest", "Dropcopy", "DropcopyRequest", "Orderflow", "OrderflowRequest", "SubscribeOrderflowRequest", "DownloadProductCatalogRequest", "DownloadProductCatalogResponse", "ExecutionInfoRequest", "ExecutionInfoResponse", "PruneExpiredSymbolsRequest", "PruneExpiredSymbolsResponse", "SubscribeSymbology", "SymbologyRequest", "SymbologySnapshot", "SymbologyUpdate", "SymbolsRequest", "SymbolsResponse", "UploadProductCatalogRequest", "UploadProductCatalogResponse", "UploadSymbologyRequest", "UploadSymbologyResponse"]
@@ -15,6 +15,12 @@ from msgspec import Meta, Struct
15
15
  from .Marketdata.Ticker import Ticker
16
16
 
17
17
 
18
+ class AccountHistoryGranularity(str, Enum):
19
+ FiveMinutes = "FiveMinutes"
20
+ Hourly = "Hourly"
21
+ Daily = "Daily"
22
+
23
+
18
24
  AccountIdOrName = str
19
25
 
20
26
 
@@ -177,6 +183,8 @@ class CandleWidth(int, Enum):
177
183
  OneSecond = 1
178
184
  FiveSecond = 5
179
185
  OneMinute = 60
186
+ TwoMinute = 120
187
+ ThreeMinute = 180
180
188
  FifteenMinute = 900
181
189
  OneHour = 3600
182
190
  OneDay = 86400
@@ -414,10 +422,16 @@ class L2BookDiff(Struct, omit_defaults=True):
414
422
 
415
423
  @property
416
424
  def datetime(self) -> datetime:
425
+ """
426
+ Convenience property to get the timestamp as a datetime object in UTC.
427
+ """
417
428
  return datetime.fromtimestamp(self.ts, tz=timezone.utc)
418
429
 
419
430
  @property
420
431
  def datetime_local(self) -> datetime:
432
+ """
433
+ Convenience property to get the timestamp as a datetime object in local time.
434
+ """
421
435
  return datetime.fromtimestamp(self.ts)
422
436
 
423
437
 
@@ -454,6 +468,7 @@ class OrderRejectReason(str, Enum):
454
468
  InsufficientCash = "InsufficientCash"
455
469
  InsufficientMargin = "InsufficientMargin"
456
470
  NotEasyToBorrow = "NotEasyToBorrow"
471
+ InvalidOrder = "InvalidOrder"
457
472
  Unknown = "Unknown"
458
473
 
459
474
 
@@ -501,6 +516,7 @@ class OrderType(str, Enum):
501
516
  LIMIT = "LIMIT"
502
517
  STOP_LOSS_LIMIT = "STOP_LOSS_LIMIT"
503
518
  TAKE_PROFIT_LIMIT = "TAKE_PROFIT_LIMIT"
519
+ BRACKET = "BRACKET"
504
520
 
505
521
 
506
522
  class ProductCatalogInfo(Struct, omit_defaults=True):
@@ -789,6 +805,42 @@ class Statement(Struct, omit_defaults=True):
789
805
  TraderIdOrEmail = str
790
806
 
791
807
 
808
+ class TriggerLimitOrderType(Struct, omit_defaults=True):
809
+ p: Annotated[Decimal, Meta(title="limit_price")]
810
+ tp: Annotated[Decimal, Meta(title="trigger_price")]
811
+
812
+ # Constructor that takes all field titles as arguments for convenience
813
+ @classmethod
814
+ def new(
815
+ cls,
816
+ limit_price: Decimal,
817
+ trigger_price: Decimal,
818
+ ):
819
+ return cls(
820
+ limit_price,
821
+ trigger_price,
822
+ )
823
+
824
+ def __str__(self) -> str:
825
+ return f"TriggerLimitOrderType(limit_price={self.p},trigger_price={self.tp})"
826
+
827
+ @property
828
+ def limit_price(self) -> Decimal:
829
+ return self.p
830
+
831
+ @limit_price.setter
832
+ def limit_price(self, value: Decimal) -> None:
833
+ self.p = value
834
+
835
+ @property
836
+ def trigger_price(self) -> Decimal:
837
+ return self.tp
838
+
839
+ @trigger_price.setter
840
+ def trigger_price(self, value: Decimal) -> None:
841
+ self.tp = value
842
+
843
+
792
844
  UserId = str
793
845
 
794
846
 
@@ -868,6 +920,9 @@ class FillKind(int, Enum):
868
920
  Correction = 2
869
921
 
870
922
 
923
+ HumanDuration = str
924
+
925
+
871
926
  class Unit(str, Enum):
872
927
  base = "base"
873
928
  quote = "quote"
@@ -1159,6 +1214,17 @@ SnapshotOrUpdateForStringAndString = Union[
1159
1214
  ]
1160
1215
 
1161
1216
 
1217
+ class SpreaderPhase(str, Enum):
1218
+ ScanningForTakes = "ScanningForTakes"
1219
+ AwaitingOrderResults = "AwaitingOrderResults"
1220
+ OrderLockout = "OrderLockout"
1221
+ NoBbo = "NoBbo"
1222
+ NotEnoughBboSize = "NotEnoughBboSize"
1223
+ DoneOverfilled = "DoneOverfilled"
1224
+ DoneAndFullyHedged = "DoneAndFullyHedged"
1225
+ DoneAndGivingUp = "DoneAndGivingUp"
1226
+
1227
+
1162
1228
  class SimpleDecimal(Struct, omit_defaults=True):
1163
1229
  simple: Decimal
1164
1230
 
@@ -1794,6 +1860,20 @@ class Fill(Struct, omit_defaults=True):
1794
1860
  def trade_time(self, value: int) -> None:
1795
1861
  self.ts = value
1796
1862
 
1863
+ @property
1864
+ def datetime(self) -> datetime:
1865
+ """
1866
+ Convenience property to get the timestamp as a datetime object in UTC.
1867
+ """
1868
+ return datetime.fromtimestamp(self.ts, tz=timezone.utc)
1869
+
1870
+ @property
1871
+ def datetime_local(self) -> datetime:
1872
+ """
1873
+ Convenience property to get the timestamp as a datetime object in local time.
1874
+ """
1875
+ return datetime.fromtimestamp(self.ts)
1876
+
1797
1877
  @property
1798
1878
  def execution_venue(self) -> str:
1799
1879
  return self.x
@@ -2255,6 +2335,100 @@ SnapshotOrUpdateForStringAndSnapshotOrUpdateForStringAndProductCatalogInfo = Uni
2255
2335
  ]
2256
2336
 
2257
2337
 
2338
+ class SpreaderParams(Struct, omit_defaults=True):
2339
+ dir: OrderDir
2340
+ leg1_marketdata_venue: str
2341
+ leg1_price_offset: Decimal
2342
+ leg1_price_ratio: Decimal
2343
+ leg1_quantity_ratio: Decimal
2344
+ leg1_symbol: str
2345
+ leg2_marketdata_venue: str
2346
+ leg2_price_offset: Decimal
2347
+ leg2_price_ratio: Decimal
2348
+ leg2_quantity_ratio: Decimal
2349
+ leg2_symbol: str
2350
+ limit_price: Decimal
2351
+ order_lockout: HumanDuration
2352
+ quantity: Decimal
2353
+ leg1_account: Optional[AccountIdOrName] = None
2354
+ leg1_execution_venue: Optional[str] = None
2355
+ leg2_account: Optional[AccountIdOrName] = None
2356
+ leg2_execution_venue: Optional[str] = None
2357
+
2358
+ # Constructor that takes all field titles as arguments for convenience
2359
+ @classmethod
2360
+ def new(
2361
+ cls,
2362
+ dir: OrderDir,
2363
+ leg1_marketdata_venue: str,
2364
+ leg1_price_offset: Decimal,
2365
+ leg1_price_ratio: Decimal,
2366
+ leg1_quantity_ratio: Decimal,
2367
+ leg1_symbol: str,
2368
+ leg2_marketdata_venue: str,
2369
+ leg2_price_offset: Decimal,
2370
+ leg2_price_ratio: Decimal,
2371
+ leg2_quantity_ratio: Decimal,
2372
+ leg2_symbol: str,
2373
+ limit_price: Decimal,
2374
+ order_lockout: HumanDuration,
2375
+ quantity: Decimal,
2376
+ leg1_account: Optional[AccountIdOrName] = None,
2377
+ leg1_execution_venue: Optional[str] = None,
2378
+ leg2_account: Optional[AccountIdOrName] = None,
2379
+ leg2_execution_venue: Optional[str] = None,
2380
+ ):
2381
+ return cls(
2382
+ dir,
2383
+ leg1_marketdata_venue,
2384
+ leg1_price_offset,
2385
+ leg1_price_ratio,
2386
+ leg1_quantity_ratio,
2387
+ leg1_symbol,
2388
+ leg2_marketdata_venue,
2389
+ leg2_price_offset,
2390
+ leg2_price_ratio,
2391
+ leg2_quantity_ratio,
2392
+ leg2_symbol,
2393
+ limit_price,
2394
+ order_lockout,
2395
+ quantity,
2396
+ leg1_account,
2397
+ leg1_execution_venue,
2398
+ leg2_account,
2399
+ leg2_execution_venue,
2400
+ )
2401
+
2402
+ def __str__(self) -> str:
2403
+ return f"SpreaderParams(dir={self.dir},leg1_marketdata_venue={self.leg1_marketdata_venue},leg1_price_offset={self.leg1_price_offset},leg1_price_ratio={self.leg1_price_ratio},leg1_quantity_ratio={self.leg1_quantity_ratio},leg1_symbol={self.leg1_symbol},leg2_marketdata_venue={self.leg2_marketdata_venue},leg2_price_offset={self.leg2_price_offset},leg2_price_ratio={self.leg2_price_ratio},leg2_quantity_ratio={self.leg2_quantity_ratio},leg2_symbol={self.leg2_symbol},limit_price={self.limit_price},order_lockout={self.order_lockout},quantity={self.quantity},leg1_account={self.leg1_account},leg1_execution_venue={self.leg1_execution_venue},leg2_account={self.leg2_account},leg2_execution_venue={self.leg2_execution_venue})"
2404
+
2405
+
2406
+ class SpreaderStatus(Struct, omit_defaults=True):
2407
+ current_spreader_phase: SpreaderPhase
2408
+ leg1_fill_quantity: Decimal
2409
+ leg2_fill_quantity: Decimal
2410
+ implied_spread_vwap: Optional[Decimal] = None
2411
+
2412
+ # Constructor that takes all field titles as arguments for convenience
2413
+ @classmethod
2414
+ def new(
2415
+ cls,
2416
+ current_spreader_phase: SpreaderPhase,
2417
+ leg1_fill_quantity: Decimal,
2418
+ leg2_fill_quantity: Decimal,
2419
+ implied_spread_vwap: Optional[Decimal] = None,
2420
+ ):
2421
+ return cls(
2422
+ current_spreader_phase,
2423
+ leg1_fill_quantity,
2424
+ leg2_fill_quantity,
2425
+ implied_spread_vwap,
2426
+ )
2427
+
2428
+ def __str__(self) -> str:
2429
+ return f"SpreaderStatus(current_spreader_phase={self.current_spreader_phase},leg1_fill_quantity={self.leg1_fill_quantity},leg2_fill_quantity={self.leg2_fill_quantity},implied_spread_vwap={self.implied_spread_vwap})"
2430
+
2431
+
2258
2432
  class Account(Struct, omit_defaults=True):
2259
2433
  id: str
2260
2434
  name: AccountName
@@ -0,0 +1,138 @@
1
+ import asyncio
2
+ import contextlib
3
+ from typing import TYPE_CHECKING, Any, AsyncGenerator, AsyncIterator, Optional, Union
4
+
5
+ import grpc.aio
6
+
7
+ from architect_py.grpc.models.Orderflow.Orderflow import Orderflow
8
+ from architect_py.grpc.models.Orderflow.OrderflowRequest import (
9
+ OrderflowRequest,
10
+ OrderflowRequest_route,
11
+ OrderflowRequestUnannotatedResponseType,
12
+ )
13
+
14
+ if TYPE_CHECKING:
15
+ from architect_py.async_client import AsyncClient
16
+
17
+ _CLOSE_SENTINEL: Any = object() # internal close marker
18
+
19
+
20
+ class OrderflowChannel:
21
+ """
22
+ Web-socket–like helper around Architect’s bidirectional Orderflow gRPC stream.
23
+
24
+ Must call OrderflowManager.start() to open the stream.
25
+
26
+ Example usage 1:
27
+ om = client.orderflow(queue_size=2048) # instantiate
28
+ await om.start() # open stream
29
+
30
+ await om.place_order(args)
31
+ update = await om.receive() # receive a single orderflow update
32
+ # to receive continuously, either but in a loop or see example usage 2.
33
+ print("first update:", update)
34
+
35
+ await om.close() # graceful shutdown, not required
36
+
37
+ Example usage 2:
38
+ async with await client.orderflow() as om: # om.start() happens automatically
39
+ async for update in om: # this will run until the stream is closed
40
+ print("order update:", update)
41
+
42
+ For more advanced usage, see the funding_rate_mean_reversion_algo.py example.
43
+ """
44
+
45
+ def __init__(self, client: "AsyncClient", *, max_queue_size: int = 1024) -> None:
46
+ self._client = client
47
+ self._send_q: asyncio.Queue[Union[OrderflowRequest, Any]] = asyncio.Queue(
48
+ maxsize=max_queue_size
49
+ )
50
+ self._receive_q: asyncio.Queue[Orderflow] = asyncio.Queue()
51
+ self._stream_task: Optional[asyncio.Task[None]] = None
52
+ self._grpc_call: Optional[
53
+ grpc.aio.StreamStreamCall[OrderflowRequest, Orderflow]
54
+ ] = None
55
+ self._closed = asyncio.Event()
56
+
57
+ async def start(self) -> None:
58
+ """ """
59
+ if self._stream_task is None:
60
+ self._stream_task = asyncio.create_task(
61
+ self._stream_loop(), name="orderflow-stream"
62
+ )
63
+
64
+ async def send(self, req: OrderflowRequest) -> None:
65
+ """
66
+ Note that some of the OrderflowRequest types are tagged versions of the classes that exist elsewhere.
67
+ Only use the TAGGED versions, as the untagged versions will not work with the stream.
68
+ """
69
+ await self._send_q.put(req)
70
+
71
+ async def receive(self) -> Orderflow:
72
+ """Await the next Orderflow update from the server."""
73
+ return await self._receive_q.get()
74
+
75
+ async def close(self) -> None:
76
+ if self._stream_task is None:
77
+ return
78
+ await self._send_q.put(_CLOSE_SENTINEL)
79
+
80
+ if self._grpc_call is not None:
81
+ self._grpc_call.cancel()
82
+
83
+ self._stream_task.cancel()
84
+ with contextlib.suppress(asyncio.CancelledError):
85
+ await self._stream_task
86
+ self._stream_task = None
87
+
88
+ async def __aenter__(self):
89
+ await self.start()
90
+ return self
91
+
92
+ async def __aexit__(self, exc_type, exc, tb):
93
+ await self.close()
94
+
95
+ def __aiter__(self) -> AsyncGenerator[Orderflow, None]:
96
+ return self._iter_updates()
97
+
98
+ async def _iter_updates(self) -> AsyncGenerator[Orderflow, None]:
99
+ while not self._closed.is_set():
100
+ try:
101
+ yield await self.receive()
102
+ except asyncio.CancelledError:
103
+ break
104
+
105
+ async def _request_iter(self) -> AsyncIterator[OrderflowRequest]:
106
+ while True:
107
+ item = await self._send_q.get()
108
+ if item is _CLOSE_SENTINEL:
109
+ break
110
+ yield item
111
+
112
+ async def _stream_loop(self) -> None:
113
+ try:
114
+ async for update in self._grpc_orderflow_stream(self._request_iter()):
115
+ await self._receive_q.put(update)
116
+ finally:
117
+ self._closed.set()
118
+
119
+ async def _grpc_orderflow_stream(
120
+ self, request_iterator: AsyncIterator[OrderflowRequest]
121
+ ) -> AsyncGenerator[Orderflow, None]:
122
+ """Low-level wrapper around Architect’s gRPC bidirectional stream."""
123
+ grpc_client = await self._client.core()
124
+ decoder = grpc_client.get_decoder(OrderflowRequestUnannotatedResponseType)
125
+
126
+ stub: grpc.aio.StreamStreamMultiCallable = grpc_client.channel.stream_stream(
127
+ OrderflowRequest_route,
128
+ request_serializer=grpc_client.encoder().encode,
129
+ response_deserializer=decoder.decode,
130
+ )
131
+
132
+ self._grpc_call = stub(
133
+ request_iterator,
134
+ metadata=(("authorization", f"Bearer {grpc_client.jwt}"),),
135
+ )
136
+
137
+ async for update in self._grpc_call:
138
+ yield update
@@ -4,18 +4,18 @@ from decimal import Decimal
4
4
  import pytest
5
5
 
6
6
  from architect_py import AsyncClient, OrderDir, TickRoundMethod
7
+ from architect_py.grpc.models.definitions import OrderType
7
8
 
8
9
 
9
10
  @pytest.mark.asyncio
10
11
  @pytest.mark.timeout(3)
11
12
  async def test_place_limit_order(async_client: AsyncClient):
12
- # CR alee: there's no good way to get the front month future
13
- symbol = "MET 20250530 CME Future/USD"
14
13
  venue = "CME"
15
- info = await async_client.get_execution_info(symbol, venue)
14
+ front_future = await async_client.get_front_future("ES CME Futures", venue)
15
+ info = await async_client.get_execution_info(front_future, venue)
16
16
  assert info is not None
17
17
  assert info.tick_size is not None
18
- snap = await async_client.get_ticker(symbol, venue)
18
+ snap = await async_client.get_ticker(front_future, venue)
19
19
  assert snap is not None
20
20
  assert snap.bid_price is not None
21
21
  accounts = await async_client.list_accounts()
@@ -23,14 +23,17 @@ async def test_place_limit_order(async_client: AsyncClient):
23
23
 
24
24
  # bid far below the best bid
25
25
  limit_price = TickRoundMethod.FLOOR(snap.bid_price * Decimal(0.9), info.tick_size)
26
- order = await async_client.place_limit_order(
27
- symbol=symbol,
26
+ order = await async_client.place_order(
27
+ symbol=front_future,
28
28
  execution_venue=venue,
29
29
  dir=OrderDir.BUY,
30
30
  quantity=Decimal(1),
31
+ order_type=OrderType.LIMIT,
31
32
  limit_price=limit_price,
33
+ post_only=False,
32
34
  account=str(account.account.id),
33
35
  )
36
+
34
37
  assert order is not None
35
38
  await asyncio.sleep(1)
36
39
  await async_client.cancel_order(order.id)
@@ -1,41 +1,130 @@
1
- """
2
1
  import asyncio
2
+ from decimal import Decimal
3
3
 
4
4
  import pytest
5
5
 
6
- from architect_py import AsyncClient
7
- from architect_py.grpc.models.Orderflow.OrderflowRequest import OrderflowRequest
6
+ from architect_py import *
7
+ from architect_py.grpc.models.Orderflow.Orderflow import TaggedOrderAck
8
+ from architect_py.grpc.models.Orderflow.OrderflowRequest import (
9
+ CancelOrder,
10
+ OrderflowRequestUnannotatedResponseType,
11
+ PlaceOrder,
12
+ )
8
13
 
9
14
 
10
- class OrderflowAsyncIterator:
11
- queue: list[OrderflowRequest]
12
- condition: asyncio.Condition
15
+ @pytest.mark.asyncio
16
+ @pytest.mark.timeout(3)
17
+ async def test_orderflow(async_client: AsyncClient):
18
+ venue = "CME"
19
+ front_future = await async_client.get_front_future("ES CME Futures", venue)
20
+ async with await async_client.orderflow() as om:
21
+ l1_snapshot = await async_client.get_l1_book_snapshot(front_future, venue=venue)
22
+ assert l1_snapshot.best_bid is not None
23
+ assert l1_snapshot.best_ask is not None
24
+ spread = l1_snapshot.best_ask[0] - l1_snapshot.best_bid[0]
25
+ price: Decimal = l1_snapshot.best_bid[0] - spread * 10
13
26
 
14
- def __init__(self):
15
- self.queue: list[OrderflowRequest] = []
16
- self.condition: asyncio.Condition = asyncio.Condition()
27
+ accounts = await async_client.list_accounts()
28
+ account = next(
29
+ acc.account.name
30
+ for acc in accounts
31
+ if not acc.account.name.startswith("RQD")
32
+ )
17
33
 
18
- def __aiter__(self):
19
- return self
34
+ await om.send(
35
+ PlaceOrder.new(
36
+ dir=OrderDir.BUY,
37
+ symbol=front_future,
38
+ time_in_force=TimeInForce.DAY,
39
+ quantity=Decimal(1),
40
+ execution_venue=venue,
41
+ order_type=OrderType.LIMIT,
42
+ limit_price=price,
43
+ post_only=False,
44
+ account=account,
45
+ )
46
+ )
47
+ print("Order placed, waiting for updates...")
48
+ i = 0
49
+ async for update in om:
50
+ assert isinstance(update, OrderflowRequestUnannotatedResponseType)
20
51
 
21
- async def __anext__(self) -> OrderflowRequest:
22
- async with self.condition:
23
- while not self.queue:
24
- await self.condition.wait()
25
- return self.queue.pop(0)
52
+ if isinstance(update, TaggedOrderAck):
53
+ await om.send(
54
+ CancelOrder(
55
+ id=update.id,
56
+ )
57
+ )
58
+ i += 1
59
+ if i > 1:
60
+ break
26
61
 
27
- async def add_to_queue(self, item: OrderflowRequest):
28
- async with self.condition:
29
- self.queue.append(item)
30
- self.condition.notify()
62
+ await async_client.cancel_all_orders()
63
+ await async_client.close()
31
64
 
32
65
 
33
66
  @pytest.mark.asyncio
34
- @pytest.mark.timeout(10)
35
- async def test_orderflow(async_client: AsyncClient):
36
- oai = OrderflowAsyncIterator()
37
- async for of in async_client.orderflow(oai):
38
- assert of is not None
39
- return
67
+ @pytest.mark.timeout(4)
68
+ async def test_stream_orderflow(async_client: AsyncClient):
69
+ print("Starting to stream Orderflow events...")
70
+
71
+ asyncio.create_task(send_order(async_client))
72
+ i = 0
73
+ async for event in async_client.stream_orderflow():
74
+ print(f"Received Orderflow event: {event}")
75
+ assert isinstance(event, OrderflowRequestUnannotatedResponseType)
76
+
77
+ if isinstance(event, OrderAck):
78
+ await async_client.cancel_order(event.order_id)
79
+
80
+ i += 1
81
+ if i > 1:
82
+ break
83
+
84
+ await async_client.close()
85
+
86
+
87
+ async def send_order(client: AsyncClient):
88
+ venue = "CME"
89
+ front_future = await client.get_front_future("ES CME Futures", venue)
90
+
91
+ l1_snapshot = await client.get_l1_book_snapshot(front_future, venue=venue)
92
+ assert l1_snapshot.best_bid is not None
93
+ assert l1_snapshot.best_ask is not None
94
+ spread = l1_snapshot.best_ask[0] - l1_snapshot.best_bid[0]
95
+ price: Decimal = l1_snapshot.best_bid[0] - spread * 10
96
+
97
+ accounts = await client.list_accounts()
98
+ account = next(
99
+ acc.account.name for acc in accounts if not acc.account.name.startswith("RQD")
100
+ )
101
+
102
+ await asyncio.sleep(0.5)
103
+ await client.place_order(
104
+ dir=OrderDir.BUY,
105
+ symbol=front_future,
106
+ time_in_force=TimeInForce.DAY,
107
+ quantity=Decimal(1),
108
+ execution_venue=venue,
109
+ order_type=OrderType.LIMIT,
110
+ limit_price=price,
111
+ post_only=False,
112
+ account=account,
113
+ )
114
+
115
+
116
+ if __name__ == "__main__":
117
+ loop = asyncio.new_event_loop()
118
+ asyncio.set_event_loop(loop)
119
+
120
+ async_client = loop.run_until_complete(
121
+ AsyncClient.connect(
122
+ api_key="",
123
+ api_secret="",
124
+ paper_trading=True,
125
+ endpoint="https://app.architect.co",
126
+ )
127
+ )
40
128
 
41
- """
129
+ loop.run_until_complete(test_stream_orderflow(async_client))
130
+ # loop.run_until_complete(test_orderflow(async_client))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: architect-py
3
- Version: 5.1.3
3
+ Version: 5.1.4
4
4
  Summary: Python SDK for the Architect trading platform and brokerage.
5
5
  Author-email: "Architect Financial Technologies, Inc." <hello@architect.co>
6
6
  License-Expression: Apache-2.0