architect-py 5.1.5__py3-none-any.whl → 5.1.6__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 (39) hide show
  1. architect_py/__init__.py +24 -4
  2. architect_py/async_client.py +66 -57
  3. architect_py/client.pyi +2 -34
  4. architect_py/grpc/models/Accounts/ResetPaperAccountRequest.py +59 -0
  5. architect_py/grpc/models/Accounts/ResetPaperAccountResponse.py +20 -0
  6. architect_py/grpc/models/Boss/OptionsTransactionsRequest.py +42 -0
  7. architect_py/grpc/models/Boss/OptionsTransactionsResponse.py +27 -0
  8. architect_py/grpc/models/OptionsMarketdata/OptionsChain.py +5 -5
  9. architect_py/grpc/models/OptionsMarketdata/OptionsChainGreeks.py +5 -5
  10. architect_py/grpc/models/OptionsMarketdata/OptionsChainGreeksRequest.py +5 -1
  11. architect_py/grpc/models/OptionsMarketdata/OptionsChainRequest.py +5 -1
  12. architect_py/grpc/models/OptionsMarketdata/OptionsContract.py +45 -0
  13. architect_py/grpc/models/OptionsMarketdata/OptionsContractGreeksRequest.py +40 -0
  14. architect_py/grpc/models/OptionsMarketdata/OptionsContractRequest.py +40 -0
  15. architect_py/grpc/models/OptionsMarketdata/OptionsExpirations.py +4 -1
  16. architect_py/grpc/models/OptionsMarketdata/OptionsExpirationsRequest.py +8 -1
  17. architect_py/grpc/models/OptionsMarketdata/OptionsGreeks.py +58 -0
  18. architect_py/grpc/models/OptionsMarketdata/OptionsWraps.py +28 -0
  19. architect_py/grpc/models/OptionsMarketdata/OptionsWrapsRequest.py +40 -0
  20. architect_py/grpc/models/__init__.py +11 -1
  21. architect_py/grpc/models/definitions.py +37 -86
  22. architect_py/grpc/orderflow.py +3 -7
  23. architect_py/grpc/server.py +1 -3
  24. architect_py/tests/test_order_entry.py +120 -1
  25. architect_py/tests/test_positions.py +208 -17
  26. {architect_py-5.1.5.dist-info → architect_py-5.1.6.dist-info}/METADATA +1 -1
  27. {architect_py-5.1.5.dist-info → architect_py-5.1.6.dist-info}/RECORD +39 -29
  28. examples/external_cpty.py +2 -1
  29. examples/funding_rate_mean_reversion_algo.py +4 -4
  30. examples/order_sending.py +3 -3
  31. examples/orderflow_channel.py +75 -56
  32. examples/stream_l1_marketdata.py +3 -1
  33. examples/stream_l2_marketdata.py +3 -1
  34. examples/tutorial_async.py +3 -2
  35. examples/tutorial_sync.py +4 -3
  36. scripts/generate_functions_md.py +2 -1
  37. {architect_py-5.1.5.dist-info → architect_py-5.1.6.dist-info}/WHEEL +0 -0
  38. {architect_py-5.1.5.dist-info → architect_py-5.1.6.dist-info}/licenses/LICENSE +0 -0
  39. {architect_py-5.1.5.dist-info → architect_py-5.1.6.dist-info}/top_level.txt +0 -0
@@ -12,8 +12,6 @@ from typing import Annotated, Dict, List, Literal, Optional, Union
12
12
 
13
13
  from msgspec import Meta, Struct
14
14
 
15
- from .Marketdata.Ticker import Ticker
16
-
17
15
 
18
16
  class AccountHistoryGranularity(str, Enum):
19
17
  FiveMinutes = "FiveMinutes"
@@ -435,6 +433,38 @@ class L2BookDiff(Struct, omit_defaults=True):
435
433
  return datetime.fromtimestamp(self.ts)
436
434
 
437
435
 
436
+ class OptionsTransaction(Struct, omit_defaults=True):
437
+ clearing_firm_account: str
438
+ quantity: Decimal
439
+ timestamp: datetime
440
+ tradable_product: str
441
+ transaction_type: str
442
+ price: Optional[Decimal] = None
443
+
444
+ # Constructor that takes all field titles as arguments for convenience
445
+ @classmethod
446
+ def new(
447
+ cls,
448
+ clearing_firm_account: str,
449
+ quantity: Decimal,
450
+ timestamp: datetime,
451
+ tradable_product: str,
452
+ transaction_type: str,
453
+ price: Optional[Decimal] = None,
454
+ ):
455
+ return cls(
456
+ clearing_firm_account,
457
+ quantity,
458
+ timestamp,
459
+ tradable_product,
460
+ transaction_type,
461
+ price,
462
+ )
463
+
464
+ def __str__(self) -> str:
465
+ return f"OptionsTransaction(clearing_firm_account={self.clearing_firm_account},quantity={self.quantity},timestamp={self.timestamp},tradable_product={self.tradable_product},transaction_type={self.transaction_type},price={self.price})"
466
+
467
+
438
468
  OrderId = Annotated[
439
469
  str, Meta(description="System-unique, persistent order identifiers")
440
470
  ]
@@ -604,6 +634,11 @@ class ProductCatalogInfo(Struct, omit_defaults=True):
604
634
  return f"ProductCatalogInfo(exchange={self.exchange},exchange_product={self.exchange_product},category={self.category},cqg_contract_symbol={self.cqg_contract_symbol},info_url={self.info_url},long_description={self.long_description},multiplier={self.multiplier},price_display_format={self.price_display_format},quote_currency={self.quote_currency},schedule_description={self.schedule_description},settle_method={self.settle_method},short_description={self.short_description},sub_category={self.sub_category})"
605
635
 
606
636
 
637
+ class PutOrCall(str, Enum):
638
+ P = "P"
639
+ C = "C"
640
+
641
+
607
642
  class RqdAccountStatistics(Struct, omit_defaults=True):
608
643
  account_number: str
609
644
  account_type: Optional[str] = None
@@ -1120,11 +1155,6 @@ class Unknown(Struct, omit_defaults=True):
1120
1155
  return f"Unknown(product_type={self.product_type})"
1121
1156
 
1122
1157
 
1123
- class PutOrCall(str, Enum):
1124
- P = "P"
1125
- C = "C"
1126
-
1127
-
1128
1158
  class SnapshotOrUpdateForStringAndProductCatalogInfo1(Struct, omit_defaults=True):
1129
1159
  snapshot: Dict[str, ProductCatalogInfo]
1130
1160
 
@@ -1955,85 +1985,6 @@ class Fill(Struct, omit_defaults=True):
1955
1985
  self.xid = value
1956
1986
 
1957
1987
 
1958
- class OptionsContract(Struct, omit_defaults=True):
1959
- expiration: date
1960
- put_or_call: PutOrCall
1961
- strike: Decimal
1962
- ticker: Ticker
1963
- underlying: str
1964
- in_the_money: Optional[bool] = None
1965
-
1966
- # Constructor that takes all field titles as arguments for convenience
1967
- @classmethod
1968
- def new(
1969
- cls,
1970
- expiration: date,
1971
- put_or_call: PutOrCall,
1972
- strike: Decimal,
1973
- ticker: Ticker,
1974
- underlying: str,
1975
- in_the_money: Optional[bool] = None,
1976
- ):
1977
- return cls(
1978
- expiration,
1979
- put_or_call,
1980
- strike,
1981
- ticker,
1982
- underlying,
1983
- in_the_money,
1984
- )
1985
-
1986
- def __str__(self) -> str:
1987
- return f"OptionsContract(expiration={self.expiration},put_or_call={self.put_or_call},strike={self.strike},ticker={self.ticker},underlying={self.underlying},in_the_money={self.in_the_money})"
1988
-
1989
-
1990
- class OptionsGreeks(Struct, omit_defaults=True):
1991
- delta: Decimal
1992
- expiration: date
1993
- gamma: Decimal
1994
- implied_volatility: Decimal
1995
- put_or_call: PutOrCall
1996
- rho: Decimal
1997
- strike: Decimal
1998
- symbol: str
1999
- theta: Decimal
2000
- underlying: str
2001
- vega: Decimal
2002
-
2003
- # Constructor that takes all field titles as arguments for convenience
2004
- @classmethod
2005
- def new(
2006
- cls,
2007
- delta: Decimal,
2008
- expiration: date,
2009
- gamma: Decimal,
2010
- implied_volatility: Decimal,
2011
- put_or_call: PutOrCall,
2012
- rho: Decimal,
2013
- strike: Decimal,
2014
- symbol: str,
2015
- theta: Decimal,
2016
- underlying: str,
2017
- vega: Decimal,
2018
- ):
2019
- return cls(
2020
- delta,
2021
- expiration,
2022
- gamma,
2023
- implied_volatility,
2024
- put_or_call,
2025
- rho,
2026
- strike,
2027
- symbol,
2028
- theta,
2029
- underlying,
2030
- vega,
2031
- )
2032
-
2033
- def __str__(self) -> str:
2034
- return f"OptionsGreeks(delta={self.delta},expiration={self.expiration},gamma={self.gamma},implied_volatility={self.implied_volatility},put_or_call={self.put_or_call},rho={self.rho},strike={self.strike},symbol={self.symbol},theta={self.theta},underlying={self.underlying},vega={self.vega})"
2035
-
2036
-
2037
1988
  class OptionsSeriesInfo(Struct, omit_defaults=True):
2038
1989
  derivative_kind: DerivativeKind
2039
1990
  exercise_type: OptionsExerciseType
@@ -4,12 +4,8 @@ from typing import TYPE_CHECKING, Any, AsyncGenerator, AsyncIterator, Optional,
4
4
 
5
5
  import grpc.aio
6
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
- )
7
+ from architect_py.grpc.models.Orderflow.Orderflow import *
8
+ from architect_py.grpc.models.Orderflow.OrderflowRequest import *
13
9
 
14
10
  if TYPE_CHECKING:
15
11
  from architect_py.async_client import AsyncClient
@@ -120,7 +116,7 @@ class OrderflowChannel:
120
116
  self, request_iterator: AsyncIterator[OrderflowRequest]
121
117
  ) -> AsyncGenerator[Orderflow, None]:
122
118
  """Low-level wrapper around Architect’s gRPC bidirectional stream."""
123
- grpc_client = await self._client.core()
119
+ grpc_client = await self._client._core()
124
120
  decoder = grpc_client.get_decoder(OrderflowRequestUnannotatedResponseType)
125
121
 
126
122
  stub: grpc.aio.StreamStreamMultiCallable = grpc_client.channel.stream_stream(
@@ -41,9 +41,7 @@ class OrderflowServicer(object):
41
41
 
42
42
 
43
43
  def add_OrderflowServicer_to_server(servicer, server):
44
- decoder = msgspec.json.Decoder(
45
- type=SubscribeOrderflowRequest.get_unannotated_response_type()
46
- )
44
+ decoder = msgspec.json.Decoder(type=SubscribeOrderflowRequest)
47
45
  rpc_method_handlers = {
48
46
  "SubscribeOrderflow": grpc.unary_stream_rpc_method_handler(
49
47
  servicer.SubscribeOrderflow,
@@ -4,7 +4,8 @@ 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
+ from architect_py.common_types.tradable_product import TradableProduct
8
+ from architect_py.grpc.models.definitions import OrderType, SpreaderParams
8
9
 
9
10
 
10
11
  @pytest.mark.asyncio
@@ -39,3 +40,121 @@ async def test_place_limit_order(async_client: AsyncClient):
39
40
  await async_client.cancel_order(order.id)
40
41
 
41
42
  await async_client.close()
43
+
44
+
45
+ @pytest.mark.asyncio
46
+ @pytest.mark.timeout(3)
47
+ async def test_place_market_order(async_client: AsyncClient):
48
+ venue = "CME"
49
+ front_future = await async_client.get_front_future("ES CME Futures", venue)
50
+ info = await async_client.get_execution_info(front_future, venue)
51
+ assert info is not None
52
+ assert info.tick_size is not None
53
+ snap = await async_client.get_ticker(front_future, venue)
54
+ assert snap is not None
55
+ assert snap.bid_price is not None
56
+ accounts = await async_client.list_accounts()
57
+ account = accounts[0]
58
+
59
+ # bid far below the best bid
60
+ order = await async_client.place_order(
61
+ symbol=front_future,
62
+ execution_venue=venue,
63
+ dir=OrderDir.BUY,
64
+ quantity=Decimal(1),
65
+ order_type=OrderType.MARKET,
66
+ account=str(account.account.id),
67
+ )
68
+
69
+ assert order is not None
70
+
71
+ await asyncio.sleep(1.5)
72
+ order = await async_client.get_order(order.id)
73
+ assert order is not None
74
+
75
+ await async_client.close()
76
+
77
+
78
+ @pytest.mark.asyncio
79
+ @pytest.mark.timeout(3)
80
+ async def test_equity_order(async_client: AsyncClient):
81
+ tradable_product = TradableProduct(base_or_value="AAPL US Equity/USD")
82
+
83
+ product_info = await async_client.get_product_info(tradable_product.base())
84
+ assert product_info is not None
85
+ venue = product_info.primary_venue
86
+ assert venue is not None
87
+
88
+ market_status = await async_client.get_market_status(tradable_product, venue)
89
+ if not market_status.is_trading:
90
+ pytest.skip(f"Market {venue} for {tradable_product} is not open")
91
+
92
+ info = await async_client.get_execution_info(tradable_product, venue)
93
+ assert info is not None
94
+ # assert info.tick_size is not None
95
+
96
+ tick_size = Decimal("0.01")
97
+
98
+ snap = await async_client.get_ticker(tradable_product, venue)
99
+ assert snap is not None
100
+ assert snap.bid_price is not None
101
+ accounts = await async_client.list_accounts()
102
+ account = accounts[0]
103
+
104
+ # bid far below the best bid
105
+ limit_price = TickRoundMethod.FLOOR(snap.bid_price * Decimal(0.9), tick_size)
106
+ order = await async_client.place_order(
107
+ symbol=tradable_product,
108
+ execution_venue=venue,
109
+ dir=OrderDir.BUY,
110
+ quantity=Decimal(1),
111
+ order_type=OrderType.LIMIT,
112
+ limit_price=limit_price,
113
+ post_only=False,
114
+ account=str(account.account.id),
115
+ )
116
+
117
+ assert order is not None
118
+ await asyncio.sleep(1)
119
+ await async_client.cancel_order(order.id)
120
+
121
+ await async_client.close()
122
+
123
+
124
+ @pytest.mark.asyncio
125
+ @pytest.mark.timeout(3)
126
+ async def test_spreader_algo(async_client: AsyncClient):
127
+ accounts = await async_client.list_accounts()
128
+ account = accounts[0]
129
+
130
+ venue = "CME"
131
+
132
+ front_ES_future = await async_client.get_front_future("ES CME Futures", venue)
133
+ front_NQ_future = await async_client.get_front_future("NQ CME Futures", venue)
134
+
135
+ params = SpreaderParams(
136
+ dir=OrderDir.BUY, # or OrderDir.SELL
137
+ leg1_marketdata_venue=venue,
138
+ leg1_price_offset=Decimal("0"),
139
+ leg1_price_ratio=Decimal("1"),
140
+ leg1_quantity_ratio=Decimal("1"),
141
+ leg1_symbol=front_ES_future,
142
+ leg2_marketdata_venue=venue,
143
+ leg2_price_offset=Decimal("0"),
144
+ leg2_price_ratio=Decimal("-1"),
145
+ leg2_quantity_ratio=Decimal("-1"),
146
+ leg2_symbol=front_NQ_future,
147
+ limit_price=Decimal("0.25"),
148
+ order_lockout="1s",
149
+ quantity=Decimal("10"),
150
+ leg1_account=account.account.id,
151
+ leg1_execution_venue=venue,
152
+ leg2_account=account.account.id,
153
+ leg2_execution_venue=venue,
154
+ )
155
+
156
+ order = await async_client.place_algo_order(params=params)
157
+
158
+ print(order)
159
+
160
+ await async_client.close()
@@ -3,32 +3,126 @@ from decimal import Decimal
3
3
 
4
4
  import pytest
5
5
 
6
- from architect_py.async_client import AsyncClient, OrderDir, OrderType
6
+ from architect_py import AsyncClient, OrderDir, OrderType, TradableProduct
7
7
 
8
+ ES_MULTIPLIER = Decimal("50.0") # ES futures multiplier
8
9
 
9
- @pytest.mark.asyncio
10
- @pytest.mark.timeout(10)
11
- async def test_positions(async_client: AsyncClient):
12
- if not async_client.paper_trading:
13
- return
14
10
 
11
+ @pytest.mark.asyncio
12
+ async def test_paper_setup(async_client: AsyncClient):
15
13
  accounts = await async_client.list_accounts()
16
14
 
17
15
  assert len(accounts) == 1, (
18
16
  f"Expected exactly one account in paper trading mode, got {len(accounts)}"
19
17
  )
20
- account_id = accounts[0].account.id
18
+
19
+ front_ES_future = await async_client.get_front_future("ES CME Futures", "CME")
20
+
21
+ product_info = await async_client.get_product_info(front_ES_future.base())
22
+ assert product_info is not None, (
23
+ f"Expected product info for {front_ES_future.base()} to be not None"
24
+ )
25
+ assert product_info.multiplier == ES_MULTIPLIER, (
26
+ f"Expected multiplier for {front_ES_future.base()} to be {ES_MULTIPLIER}, got {product_info.multiplier}"
27
+ )
28
+ await async_client.close()
29
+
30
+
31
+ @pytest.mark.asyncio
32
+ async def test_flattening_position(async_client: AsyncClient):
33
+ if not async_client.paper_trading:
34
+ return
35
+
21
36
  front_ES_future = await async_client.get_front_future("ES CME Futures", "CME")
37
+ [account] = await async_client.list_accounts()
38
+ account_id = account.account.id
39
+
40
+ market_status = await async_client.get_market_status(front_ES_future, "CME")
41
+ if not market_status.is_trading:
42
+ await async_client.close()
43
+ pytest.skip(
44
+ f"Market for {front_ES_future} is not trading, skipping test_flattening_position"
45
+ )
46
+
47
+ await async_client.place_order(
48
+ symbol=front_ES_future,
49
+ venue="CME",
50
+ dir=OrderDir.BUY,
51
+ quantity=Decimal(value="100"),
52
+ account=account_id,
53
+ order_type=OrderType.MARKET,
54
+ )
55
+
56
+ positions = await async_client.get_positions(accounts=[account_id])
57
+
58
+ for tp, position in positions.items():
59
+ tradable_product = TradableProduct(tp)
60
+ product_info = await async_client.get_product_info(tradable_product.base())
61
+ assert product_info is not None, (
62
+ f"Expected product info for {tradable_product.base()} to be not None"
63
+ )
64
+ venue = product_info.primary_venue
65
+ assert venue is not None, (
66
+ f"Expected primary venue for {tradable_product.base()} to be not None"
67
+ )
68
+
69
+ market_status = await async_client.get_market_status(
70
+ symbol=tp,
71
+ venue=venue,
72
+ )
73
+ if not market_status.is_trading:
74
+ continue
75
+
76
+ if position != Decimal(0):
77
+ flatten_direction = OrderDir.SELL if position > Decimal(0) else OrderDir.BUY
78
+
79
+ await async_client.place_order(
80
+ symbol=tp,
81
+ dir=flatten_direction,
82
+ quantity=abs(position),
83
+ account=account_id,
84
+ order_type=OrderType.MARKET,
85
+ )
86
+
87
+ await asyncio.sleep(1.5) # wait for orders to be processed
88
+
89
+ positions = await async_client.get_positions(accounts=[account_id])
90
+ assert len(positions) == 0, (
91
+ f"Expected no positions in paper trading mode, got {len(positions)}"
92
+ )
93
+ await async_client.close()
94
+
95
+
96
+ @pytest.mark.asyncio
97
+ @pytest.mark.timeout(10)
98
+ async def test_paper_positions(async_client: AsyncClient):
99
+ venue = "CME"
100
+ if not async_client.paper_trading:
101
+ return
102
+
103
+ [account] = await async_client.list_accounts()
104
+ account_id = account.account.id
105
+ front_ES_future = await async_client.get_front_future("ES CME Futures", venue)
22
106
  positions = await async_client.get_positions(accounts=[account_id])
23
107
  ES_position = positions.get(front_ES_future)
24
108
 
109
+ market_status = await async_client.get_market_status(
110
+ symbol=front_ES_future,
111
+ venue=venue,
112
+ )
113
+ if not market_status.is_trading:
114
+ await async_client.close()
115
+ pytest.skip(
116
+ f"Market for {front_ES_future} is not trading, skipping test_paper_pnl"
117
+ )
118
+
25
119
  # flatten position
26
120
  if ES_position is not None:
27
121
  flatten_direction = OrderDir.SELL if ES_position > Decimal(0) else OrderDir.BUY
28
122
 
29
123
  order = await async_client.place_order(
30
124
  symbol=front_ES_future,
31
- venue="CME",
125
+ venue=venue,
32
126
  dir=flatten_direction,
33
127
  quantity=Decimal(value="1"),
34
128
  account=account_id,
@@ -49,7 +143,7 @@ async def test_positions(async_client: AsyncClient):
49
143
  # go long
50
144
  order = await async_client.place_order(
51
145
  symbol=front_ES_future,
52
- venue="CME",
146
+ venue=venue,
53
147
  dir=OrderDir.BUY,
54
148
  quantity=Decimal(value="5"),
55
149
  account=account_id,
@@ -63,7 +157,7 @@ async def test_positions(async_client: AsyncClient):
63
157
  # go long to flat
64
158
  order = await async_client.place_order(
65
159
  symbol=front_ES_future,
66
- venue="CME",
160
+ venue=venue,
67
161
  dir=OrderDir.SELL,
68
162
  quantity=Decimal(value="5"),
69
163
  account=account_id,
@@ -77,7 +171,7 @@ async def test_positions(async_client: AsyncClient):
77
171
  # go long
78
172
  order = await async_client.place_order(
79
173
  symbol=front_ES_future,
80
- venue="CME",
174
+ venue=venue,
81
175
  dir=OrderDir.BUY,
82
176
  quantity=Decimal(value="8"),
83
177
  account=account_id,
@@ -91,7 +185,7 @@ async def test_positions(async_client: AsyncClient):
91
185
  # go long to short
92
186
  order = await async_client.place_order(
93
187
  symbol=front_ES_future,
94
- venue="CME",
188
+ venue=venue,
95
189
  dir=OrderDir.SELL,
96
190
  quantity=Decimal(value="10"),
97
191
  account=account_id,
@@ -105,7 +199,7 @@ async def test_positions(async_client: AsyncClient):
105
199
  # go flat
106
200
  order = await async_client.place_order(
107
201
  symbol=front_ES_future,
108
- venue="CME",
202
+ venue=venue,
109
203
  dir=OrderDir.BUY,
110
204
  quantity=Decimal(value="2"),
111
205
  account=account_id,
@@ -119,7 +213,7 @@ async def test_positions(async_client: AsyncClient):
119
213
  # go short
120
214
  order = await async_client.place_order(
121
215
  symbol=front_ES_future,
122
- venue="CME",
216
+ venue=venue,
123
217
  dir=OrderDir.SELL,
124
218
  quantity=Decimal(value="5"),
125
219
  account=account_id,
@@ -133,7 +227,7 @@ async def test_positions(async_client: AsyncClient):
133
227
  # go short to flat
134
228
  order = await async_client.place_order(
135
229
  symbol=front_ES_future,
136
- venue="CME",
230
+ venue=venue,
137
231
  dir=OrderDir.BUY,
138
232
  quantity=Decimal(value="5"),
139
233
  account=account_id,
@@ -147,7 +241,7 @@ async def test_positions(async_client: AsyncClient):
147
241
  # go short
148
242
  order = await async_client.place_order(
149
243
  symbol=front_ES_future,
150
- venue="CME",
244
+ venue=venue,
151
245
  dir=OrderDir.SELL,
152
246
  quantity=Decimal(value="5"),
153
247
  account=account_id,
@@ -161,7 +255,7 @@ async def test_positions(async_client: AsyncClient):
161
255
  # go short to long
162
256
  order = await async_client.place_order(
163
257
  symbol=front_ES_future,
164
- venue="CME",
258
+ venue=venue,
165
259
  dir=OrderDir.BUY,
166
260
  quantity=Decimal(value="10"),
167
261
  account=account_id,
@@ -171,3 +265,100 @@ async def test_positions(async_client: AsyncClient):
171
265
  assert positions.get(front_ES_future) == Decimal(5), (
172
266
  f"Expected position in {front_ES_future} to be 5, got {positions.get(front_ES_future)}"
173
267
  )
268
+ await async_client.close()
269
+
270
+
271
+ @pytest.mark.asyncio
272
+ @pytest.mark.timeout(10)
273
+ async def test_paper_pnl(async_client: AsyncClient):
274
+ if not async_client.paper_trading:
275
+ return
276
+
277
+ [account] = await async_client.list_accounts()
278
+ account_id = account.account.id
279
+ front_ES_future = await async_client.get_front_future("ES CME Futures", "CME")
280
+ positions = await async_client.get_positions(accounts=[account_id])
281
+ ES_position = positions.get(front_ES_future)
282
+
283
+ market_status = await async_client.get_market_status(
284
+ symbol=front_ES_future,
285
+ venue="CME",
286
+ )
287
+ if not market_status.is_trading:
288
+ await async_client.close()
289
+ pytest.skip(
290
+ f"Market for {front_ES_future} is not trading, skipping test_paper_pnl"
291
+ )
292
+
293
+ # flatten position
294
+ if ES_position is not None:
295
+ flatten_direction = OrderDir.SELL if ES_position > Decimal(0) else OrderDir.BUY
296
+
297
+ quantity = abs(ES_position)
298
+
299
+ order = await async_client.place_order(
300
+ symbol=front_ES_future,
301
+ venue="CME",
302
+ dir=flatten_direction,
303
+ quantity=quantity,
304
+ account=account_id,
305
+ order_type=OrderType.MARKET,
306
+ )
307
+ while True:
308
+ open_orders = await async_client.get_open_orders(order_ids=[order.id])
309
+ if not open_orders:
310
+ break
311
+ await asyncio.sleep(0.2)
312
+
313
+ position = await async_client.get_positions(accounts=[account_id])
314
+ assert len(position) == 0, (
315
+ f"Expected no positions in paper trading mode, got {position}"
316
+ )
317
+
318
+ account_summary = await async_client.get_account_summary(account_id)
319
+ assert account_summary.purchasing_power is not None, (
320
+ "Expected purchasing power after trades to be set, got None"
321
+ )
322
+
323
+ pre_purchasing_power = account_summary.purchasing_power
324
+
325
+ quantity = Decimal(value="7")
326
+
327
+ sell_order = await async_client.place_order(
328
+ symbol=front_ES_future,
329
+ venue="CME",
330
+ dir=OrderDir.SELL,
331
+ quantity=quantity,
332
+ account=account_id,
333
+ order_type=OrderType.MARKET,
334
+ )
335
+
336
+ buy_order = await async_client.place_order(
337
+ symbol=front_ES_future,
338
+ venue="CME",
339
+ dir=OrderDir.BUY,
340
+ quantity=quantity,
341
+ account=account_id,
342
+ order_type=OrderType.MARKET,
343
+ )
344
+ await asyncio.sleep(1.5) # wait for order to be processed
345
+
346
+ sell_fill = await async_client.get_fills(order_id=sell_order.id)
347
+ sell_fill_price = sell_fill.fills[0].price
348
+
349
+ buy_fill = await async_client.get_fills(order_id=buy_order.id)
350
+ buy_fill_price = buy_fill.fills[0].price
351
+
352
+ pnl = (sell_fill_price - buy_fill_price) * ES_MULTIPLIER * quantity
353
+
354
+ account_summary = await async_client.get_account_summary(account_id)
355
+ assert account_summary.purchasing_power is not None, (
356
+ "Expected purchasing power after trades to be set, got None"
357
+ )
358
+ post_purchasing_power = account_summary.purchasing_power
359
+
360
+ assert post_purchasing_power == pre_purchasing_power + pnl, (
361
+ f"Expected purchasing power to be {pre_purchasing_power + pnl}, got {post_purchasing_power}.\n"
362
+ f"Buy fill price: {buy_fill_price}, Sell fill price: {sell_fill_price}, quantity: {quantity}, pnl: {pnl}, pre_purchasing_power: {pre_purchasing_power}, post_purchasing_power: {post_purchasing_power}"
363
+ )
364
+ await async_client.close()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: architect-py
3
- Version: 5.1.5
3
+ Version: 5.1.6
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