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.
- architect_py/__init__.py +24 -4
- architect_py/async_client.py +66 -57
- architect_py/client.pyi +2 -34
- architect_py/grpc/models/Accounts/ResetPaperAccountRequest.py +59 -0
- architect_py/grpc/models/Accounts/ResetPaperAccountResponse.py +20 -0
- architect_py/grpc/models/Boss/OptionsTransactionsRequest.py +42 -0
- architect_py/grpc/models/Boss/OptionsTransactionsResponse.py +27 -0
- architect_py/grpc/models/OptionsMarketdata/OptionsChain.py +5 -5
- architect_py/grpc/models/OptionsMarketdata/OptionsChainGreeks.py +5 -5
- architect_py/grpc/models/OptionsMarketdata/OptionsChainGreeksRequest.py +5 -1
- architect_py/grpc/models/OptionsMarketdata/OptionsChainRequest.py +5 -1
- architect_py/grpc/models/OptionsMarketdata/OptionsContract.py +45 -0
- architect_py/grpc/models/OptionsMarketdata/OptionsContractGreeksRequest.py +40 -0
- architect_py/grpc/models/OptionsMarketdata/OptionsContractRequest.py +40 -0
- architect_py/grpc/models/OptionsMarketdata/OptionsExpirations.py +4 -1
- architect_py/grpc/models/OptionsMarketdata/OptionsExpirationsRequest.py +8 -1
- architect_py/grpc/models/OptionsMarketdata/OptionsGreeks.py +58 -0
- architect_py/grpc/models/OptionsMarketdata/OptionsWraps.py +28 -0
- architect_py/grpc/models/OptionsMarketdata/OptionsWrapsRequest.py +40 -0
- architect_py/grpc/models/__init__.py +11 -1
- architect_py/grpc/models/definitions.py +37 -86
- architect_py/grpc/orderflow.py +3 -7
- architect_py/grpc/server.py +1 -3
- architect_py/tests/test_order_entry.py +120 -1
- architect_py/tests/test_positions.py +208 -17
- {architect_py-5.1.5.dist-info → architect_py-5.1.6.dist-info}/METADATA +1 -1
- {architect_py-5.1.5.dist-info → architect_py-5.1.6.dist-info}/RECORD +39 -29
- examples/external_cpty.py +2 -1
- examples/funding_rate_mean_reversion_algo.py +4 -4
- examples/order_sending.py +3 -3
- examples/orderflow_channel.py +75 -56
- examples/stream_l1_marketdata.py +3 -1
- examples/stream_l2_marketdata.py +3 -1
- examples/tutorial_async.py +3 -2
- examples/tutorial_sync.py +4 -3
- scripts/generate_functions_md.py +2 -1
- {architect_py-5.1.5.dist-info → architect_py-5.1.6.dist-info}/WHEEL +0 -0
- {architect_py-5.1.5.dist-info → architect_py-5.1.6.dist-info}/licenses/LICENSE +0 -0
- {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
|
architect_py/grpc/orderflow.py
CHANGED
@@ -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
|
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.
|
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(
|
architect_py/grpc/server.py
CHANGED
@@ -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.
|
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
|
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
|
-
|
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=
|
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=
|
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=
|
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=
|
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=
|
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=
|
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=
|
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=
|
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=
|
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=
|
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()
|