architect-py 5.1.3__py3-none-any.whl → 5.1.4rc1__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.
@@ -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
@@ -454,6 +462,7 @@ class OrderRejectReason(str, Enum):
454
462
  InsufficientCash = "InsufficientCash"
455
463
  InsufficientMargin = "InsufficientMargin"
456
464
  NotEasyToBorrow = "NotEasyToBorrow"
465
+ InvalidOrder = "InvalidOrder"
457
466
  Unknown = "Unknown"
458
467
 
459
468
 
@@ -501,6 +510,7 @@ class OrderType(str, Enum):
501
510
  LIMIT = "LIMIT"
502
511
  STOP_LOSS_LIMIT = "STOP_LOSS_LIMIT"
503
512
  TAKE_PROFIT_LIMIT = "TAKE_PROFIT_LIMIT"
513
+ BRACKET = "BRACKET"
504
514
 
505
515
 
506
516
  class ProductCatalogInfo(Struct, omit_defaults=True):
@@ -789,6 +799,42 @@ class Statement(Struct, omit_defaults=True):
789
799
  TraderIdOrEmail = str
790
800
 
791
801
 
802
+ class TriggerLimitOrderType(Struct, omit_defaults=True):
803
+ p: Annotated[Decimal, Meta(title="limit_price")]
804
+ tp: Annotated[Decimal, Meta(title="trigger_price")]
805
+
806
+ # Constructor that takes all field titles as arguments for convenience
807
+ @classmethod
808
+ def new(
809
+ cls,
810
+ limit_price: Decimal,
811
+ trigger_price: Decimal,
812
+ ):
813
+ return cls(
814
+ limit_price,
815
+ trigger_price,
816
+ )
817
+
818
+ def __str__(self) -> str:
819
+ return f"TriggerLimitOrderType(limit_price={self.p},trigger_price={self.tp})"
820
+
821
+ @property
822
+ def limit_price(self) -> Decimal:
823
+ return self.p
824
+
825
+ @limit_price.setter
826
+ def limit_price(self, value: Decimal) -> None:
827
+ self.p = value
828
+
829
+ @property
830
+ def trigger_price(self) -> Decimal:
831
+ return self.tp
832
+
833
+ @trigger_price.setter
834
+ def trigger_price(self, value: Decimal) -> None:
835
+ self.tp = value
836
+
837
+
792
838
  UserId = str
793
839
 
794
840
 
@@ -868,6 +914,9 @@ class FillKind(int, Enum):
868
914
  Correction = 2
869
915
 
870
916
 
917
+ HumanDuration = str
918
+
919
+
871
920
  class Unit(str, Enum):
872
921
  base = "base"
873
922
  quote = "quote"
@@ -1159,6 +1208,17 @@ SnapshotOrUpdateForStringAndString = Union[
1159
1208
  ]
1160
1209
 
1161
1210
 
1211
+ class SpreaderPhase(str, Enum):
1212
+ ScanningForTakes = "ScanningForTakes"
1213
+ AwaitingOrderResults = "AwaitingOrderResults"
1214
+ OrderLockout = "OrderLockout"
1215
+ NoBbo = "NoBbo"
1216
+ NotEnoughBboSize = "NotEnoughBboSize"
1217
+ DoneOverfilled = "DoneOverfilled"
1218
+ DoneAndFullyHedged = "DoneAndFullyHedged"
1219
+ DoneAndGivingUp = "DoneAndGivingUp"
1220
+
1221
+
1162
1222
  class SimpleDecimal(Struct, omit_defaults=True):
1163
1223
  simple: Decimal
1164
1224
 
@@ -2255,6 +2315,100 @@ SnapshotOrUpdateForStringAndSnapshotOrUpdateForStringAndProductCatalogInfo = Uni
2255
2315
  ]
2256
2316
 
2257
2317
 
2318
+ class SpreaderParams(Struct, omit_defaults=True):
2319
+ dir: OrderDir
2320
+ leg1_marketdata_venue: str
2321
+ leg1_price_offset: Decimal
2322
+ leg1_price_ratio: Decimal
2323
+ leg1_quantity_ratio: Decimal
2324
+ leg1_symbol: str
2325
+ leg2_marketdata_venue: str
2326
+ leg2_price_offset: Decimal
2327
+ leg2_price_ratio: Decimal
2328
+ leg2_quantity_ratio: Decimal
2329
+ leg2_symbol: str
2330
+ limit_price: Decimal
2331
+ order_lockout: HumanDuration
2332
+ quantity: Decimal
2333
+ leg1_account: Optional[AccountIdOrName] = None
2334
+ leg1_execution_venue: Optional[str] = None
2335
+ leg2_account: Optional[AccountIdOrName] = None
2336
+ leg2_execution_venue: Optional[str] = None
2337
+
2338
+ # Constructor that takes all field titles as arguments for convenience
2339
+ @classmethod
2340
+ def new(
2341
+ cls,
2342
+ dir: OrderDir,
2343
+ leg1_marketdata_venue: str,
2344
+ leg1_price_offset: Decimal,
2345
+ leg1_price_ratio: Decimal,
2346
+ leg1_quantity_ratio: Decimal,
2347
+ leg1_symbol: str,
2348
+ leg2_marketdata_venue: str,
2349
+ leg2_price_offset: Decimal,
2350
+ leg2_price_ratio: Decimal,
2351
+ leg2_quantity_ratio: Decimal,
2352
+ leg2_symbol: str,
2353
+ limit_price: Decimal,
2354
+ order_lockout: HumanDuration,
2355
+ quantity: Decimal,
2356
+ leg1_account: Optional[AccountIdOrName] = None,
2357
+ leg1_execution_venue: Optional[str] = None,
2358
+ leg2_account: Optional[AccountIdOrName] = None,
2359
+ leg2_execution_venue: Optional[str] = None,
2360
+ ):
2361
+ return cls(
2362
+ dir,
2363
+ leg1_marketdata_venue,
2364
+ leg1_price_offset,
2365
+ leg1_price_ratio,
2366
+ leg1_quantity_ratio,
2367
+ leg1_symbol,
2368
+ leg2_marketdata_venue,
2369
+ leg2_price_offset,
2370
+ leg2_price_ratio,
2371
+ leg2_quantity_ratio,
2372
+ leg2_symbol,
2373
+ limit_price,
2374
+ order_lockout,
2375
+ quantity,
2376
+ leg1_account,
2377
+ leg1_execution_venue,
2378
+ leg2_account,
2379
+ leg2_execution_venue,
2380
+ )
2381
+
2382
+ def __str__(self) -> str:
2383
+ 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})"
2384
+
2385
+
2386
+ class SpreaderStatus(Struct, omit_defaults=True):
2387
+ current_spreader_phase: SpreaderPhase
2388
+ leg1_fill_quantity: Decimal
2389
+ leg2_fill_quantity: Decimal
2390
+ implied_spread_vwap: Optional[Decimal] = None
2391
+
2392
+ # Constructor that takes all field titles as arguments for convenience
2393
+ @classmethod
2394
+ def new(
2395
+ cls,
2396
+ current_spreader_phase: SpreaderPhase,
2397
+ leg1_fill_quantity: Decimal,
2398
+ leg2_fill_quantity: Decimal,
2399
+ implied_spread_vwap: Optional[Decimal] = None,
2400
+ ):
2401
+ return cls(
2402
+ current_spreader_phase,
2403
+ leg1_fill_quantity,
2404
+ leg2_fill_quantity,
2405
+ implied_spread_vwap,
2406
+ )
2407
+
2408
+ def __str__(self) -> str:
2409
+ 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})"
2410
+
2411
+
2258
2412
  class Account(Struct, omit_defaults=True):
2259
2413
  id: str
2260
2414
  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.4rc1
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