Skip to content

Commit b5dc8dd

Browse files
authored
add get complex orders, get live complex orders (#156)
1 parent f497dc8 commit b5dc8dd

File tree

3 files changed

+104
-31
lines changed

3 files changed

+104
-31
lines changed

tastytrade/account.py

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -832,11 +832,11 @@ def get_margin_requirements(self, session: Session) -> MarginReport:
832832

833833
def get_live_orders(self, session: Session) -> List[PlacedOrder]:
834834
"""
835-
Get all live orders for the account.
835+
Get orders placed today for the account.
836836
837837
:param session: the session to use for the request.
838838
839-
:return: a list of :class:`Order` objects.
839+
:return: a list of :class:`PlacedOrder` objects.
840840
"""
841841
response = requests.get(
842842
f'{session.base_url}/accounts/{self.account_number}/orders/live',
@@ -848,6 +848,28 @@ def get_live_orders(self, session: Session) -> List[PlacedOrder]:
848848

849849
return [PlacedOrder(**entry) for entry in data]
850850

851+
def get_live_complex_orders(
852+
self,
853+
session: Session
854+
) -> List[PlacedComplexOrder]:
855+
"""
856+
Get complex orders placed today for the account.
857+
858+
:param session: the session to use for the request.
859+
860+
:return: a list of :class:`PlacedComplexOrder` objects.
861+
"""
862+
response = requests.get(
863+
(f'{session.base_url}/accounts/{self.account_number}'
864+
f'/complex-orders/live'),
865+
headers=session.headers
866+
)
867+
validate_response(response)
868+
869+
data = response.json()['data']['items']
870+
871+
return [PlacedComplexOrder(**entry) for entry in data]
872+
851873
def get_complex_order(
852874
self,
853875
session: Session,
@@ -998,6 +1020,57 @@ def get_order_history(
9981020

9991021
return [PlacedOrder(**entry) for entry in results]
10001022

1023+
def get_complex_order_history(
1024+
self,
1025+
session: Session,
1026+
per_page: int = 50,
1027+
page_offset: Optional[int] = None
1028+
) -> List[PlacedComplexOrder]:
1029+
"""
1030+
Get order history of the account.
1031+
1032+
:param session: the session to use for the request.
1033+
:param per_page: the number of results to return per page.
1034+
:param page_offset:
1035+
provide a specific page to get; if not provided, get all pages
1036+
1037+
:return:
1038+
a list of Tastytrade 'PlacedComplexOrder' objects in JSON format.
1039+
"""
1040+
# if a specific page is provided, we just get that page;
1041+
# otherwise, we loop through all pages
1042+
paginate = False
1043+
if page_offset is None:
1044+
page_offset = 0
1045+
paginate = True
1046+
params: Dict[str, Any] = {
1047+
'per-page': per_page,
1048+
'page-offset': page_offset
1049+
}
1050+
1051+
# loop through pages and get all transactions
1052+
results = []
1053+
while True:
1054+
response = requests.get(
1055+
(f'{session.base_url}/accounts/{self.account_number}'
1056+
f'/complex-orders'),
1057+
headers=session.headers,
1058+
params={k: v for k, v in params.items() if v is not None}
1059+
)
1060+
validate_response(response)
1061+
1062+
json = response.json()
1063+
results.extend(json['data']['items'])
1064+
1065+
pagination = json['pagination']
1066+
if pagination['page-offset'] >= pagination['total-pages'] - 1:
1067+
break
1068+
if not paginate:
1069+
break
1070+
params['page-offset'] += 1 # type: ignore
1071+
1072+
return [PlacedComplexOrder(**entry) for entry in results]
1073+
10011074
def place_order(
10021075
self,
10031076
session: Session,

tastytrade/session.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,14 +148,17 @@ class ProductionSession(Session):
148148
:param two_factor_authentication:
149149
if two factor authentication is enabled, this is the code sent to the
150150
user's device
151+
:param dxfeed_tos_compliant:
152+
whether to use the dxfeed TOS-compliant API endpoint for the streamer
151153
"""
152154
def __init__(
153155
self,
154156
login: str,
155157
password: Optional[str] = None,
156158
remember_me: bool = False,
157159
remember_token: Optional[str] = None,
158-
two_factor_authentication: Optional[str] = None
160+
two_factor_authentication: Optional[str] = None,
161+
dxfeed_tos_compliant: bool = False
159162
):
160163
body = {
161164
'login': login,
@@ -203,8 +206,11 @@ def __init__(
203206
self.validate()
204207

205208
# Pull streamer tokens and urls
209+
url = ('api-quote-tokens'
210+
if dxfeed_tos_compliant
211+
else 'quote-streamer-tokens')
206212
response = requests.get(
207-
f'{self.base_url}/quote-streamer-tokens',
213+
f'{self.base_url}/{url}',
208214
headers=self.headers
209215
)
210216
validate_response(response)

tests/test_account.py

Lines changed: 21 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,16 @@ def account(session):
1515
return Account.get_accounts(session)[0]
1616

1717

18-
@pytest.fixture(scope='session')
18+
@pytest.fixture
1919
def cert_session(get_cert_credentials):
2020
usr, pwd = get_cert_credentials
2121
session = CertificationSession(usr, pwd)
2222
yield session
2323
session.destroy()
2424

2525

26-
@pytest.fixture(scope='session')
27-
def cert_account(cert_session):
28-
return Account.get_account(cert_session, '5WZ97189')
26+
def test_cert_accounts(cert_session):
27+
assert Account.get_accounts(cert_session) != []
2928

3029

3130
def test_get_account(session, account):
@@ -75,14 +74,14 @@ def test_get_effective_margin_requirements(session, account):
7574

7675
@pytest.fixture(scope='session')
7776
def new_order(session):
78-
symbol = Equity.get_equity(session, 'SPY')
77+
symbol = Equity.get_equity(session, 'NVDA')
7978
leg = symbol.build_leg(Decimal(1), OrderAction.BUY_TO_OPEN)
8079

8180
return NewOrder(
8281
time_in_force=OrderTimeInForce.DAY,
8382
order_type=OrderType.LIMIT,
8483
legs=[leg],
85-
price=Decimal(42), # if this fills the US has crumbled
84+
price=Decimal(10), # if this fills the US has crumbled
8685
price_effect=PriceEffect.DEBIT
8786
)
8887

@@ -104,7 +103,7 @@ def test_get_order(session, account, placed_order):
104103

105104
def test_replace_and_delete_order(session, account, new_order, placed_order):
106105
modified_order = new_order.model_copy()
107-
modified_order.price = Decimal(40)
106+
modified_order.price = Decimal(11)
108107
replaced = account.replace_order(session, placed_order.id, modified_order)
109108
sleep(3)
110109
account.delete_order(session, replaced.id)
@@ -114,25 +113,17 @@ def test_get_order_history(session, account):
114113
account.get_order_history(session, page_offset=0)
115114

116115

116+
def test_get_complex_order_history(session, account):
117+
account.get_complex_order_history(session, page_offset=0)
118+
119+
117120
def test_get_live_orders(session, account):
118121
account.get_live_orders(session)
119122

120123

121-
def test_place_oco_order(cert_session, cert_account):
122-
session = cert_session
123-
account = cert_account
124-
# first, buy share of SPY to set up the OCO order
125-
symbol = Equity.get_equity(session, 'SPY')
126-
opening = symbol.build_leg(Decimal(1), OrderAction.BUY_TO_OPEN)
127-
resp1 = account.place_order(session, NewOrder(
128-
time_in_force=OrderTimeInForce.DAY,
129-
order_type=OrderType.LIMIT,
130-
legs=[opening],
131-
price=Decimal('2.5'), # should fill immediately for cert account
132-
price_effect=PriceEffect.DEBIT
133-
), dry_run=False)
134-
assert resp1.order.status != OrderStatus.REJECTED
135-
124+
def test_place_oco_order(session, account):
125+
# account must have a share of NVDA for this to work
126+
symbol = Equity.get_equity(session, 'NVDA')
136127
closing = symbol.build_leg(Decimal(1), OrderAction.SELL_TO_CLOSE)
137128
oco = NewComplexOrder(
138129
orders=[
@@ -159,9 +150,7 @@ def test_place_oco_order(cert_session, cert_account):
159150
account.delete_complex_order(session, resp2.complex_order.id)
160151

161152

162-
def test_place_otoco_order(cert_session, cert_account):
163-
session = cert_session
164-
account = cert_account
153+
def test_place_otoco_order(session, account):
165154
symbol = Equity.get_equity(session, 'AAPL')
166155
opening = symbol.build_leg(Decimal(1), OrderAction.BUY_TO_OPEN)
167156
closing = symbol.build_leg(Decimal(1), OrderAction.SELL_TO_CLOSE)
@@ -170,15 +159,15 @@ def test_place_otoco_order(cert_session, cert_account):
170159
time_in_force=OrderTimeInForce.DAY,
171160
order_type=OrderType.LIMIT,
172161
legs=[opening],
173-
price=Decimal('250'), # won't fill
162+
price=Decimal('100'), # won't fill
174163
price_effect=PriceEffect.DEBIT
175164
),
176165
orders=[
177166
NewOrder(
178167
time_in_force=OrderTimeInForce.GTC,
179168
order_type=OrderType.LIMIT,
180169
legs=[closing],
181-
price=Decimal('2500'), # won't fill
170+
price=Decimal('400'), # won't fill
182171
price_effect=PriceEffect.CREDIT
183172
),
184173
NewOrder(
@@ -193,3 +182,8 @@ def test_place_otoco_order(cert_session, cert_account):
193182
resp = account.place_complex_order(session, otoco, dry_run=False)
194183
sleep(3)
195184
account.delete_complex_order(session, resp.complex_order.id)
185+
186+
187+
def test_get_live_complex_orders(session, account):
188+
orders = account.get_live_complex_orders(session)
189+
assert orders != []

0 commit comments

Comments
 (0)