Skip to content

Commit

Permalink
add get complex orders, get live complex orders (#156)
Browse files Browse the repository at this point in the history
  • Loading branch information
Graeme22 authored Jul 8, 2024
1 parent f497dc8 commit b5dc8dd
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 31 deletions.
77 changes: 75 additions & 2 deletions tastytrade/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -832,11 +832,11 @@ def get_margin_requirements(self, session: Session) -> MarginReport:

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

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

def get_live_complex_orders(
self,
session: Session
) -> List[PlacedComplexOrder]:
"""
Get complex orders placed today for the account.
:param session: the session to use for the request.
:return: a list of :class:`PlacedComplexOrder` objects.
"""
response = requests.get(
(f'{session.base_url}/accounts/{self.account_number}'
f'/complex-orders/live'),
headers=session.headers
)
validate_response(response)

data = response.json()['data']['items']

return [PlacedComplexOrder(**entry) for entry in data]

def get_complex_order(
self,
session: Session,
Expand Down Expand Up @@ -998,6 +1020,57 @@ def get_order_history(

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

def get_complex_order_history(
self,
session: Session,
per_page: int = 50,
page_offset: Optional[int] = None
) -> List[PlacedComplexOrder]:
"""
Get order history of the account.
:param session: the session to use for the request.
:param per_page: the number of results to return per page.
:param page_offset:
provide a specific page to get; if not provided, get all pages
:return:
a list of Tastytrade 'PlacedComplexOrder' objects in JSON format.
"""
# if a specific page is provided, we just get that page;
# otherwise, we loop through all pages
paginate = False
if page_offset is None:
page_offset = 0
paginate = True
params: Dict[str, Any] = {
'per-page': per_page,
'page-offset': page_offset
}

# loop through pages and get all transactions
results = []
while True:
response = requests.get(
(f'{session.base_url}/accounts/{self.account_number}'
f'/complex-orders'),
headers=session.headers,
params={k: v for k, v in params.items() if v is not None}
)
validate_response(response)

json = response.json()
results.extend(json['data']['items'])

pagination = json['pagination']
if pagination['page-offset'] >= pagination['total-pages'] - 1:
break
if not paginate:
break
params['page-offset'] += 1 # type: ignore

return [PlacedComplexOrder(**entry) for entry in results]

def place_order(
self,
session: Session,
Expand Down
10 changes: 8 additions & 2 deletions tastytrade/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,14 +148,17 @@ class ProductionSession(Session):
:param two_factor_authentication:
if two factor authentication is enabled, this is the code sent to the
user's device
:param dxfeed_tos_compliant:
whether to use the dxfeed TOS-compliant API endpoint for the streamer
"""
def __init__(
self,
login: str,
password: Optional[str] = None,
remember_me: bool = False,
remember_token: Optional[str] = None,
two_factor_authentication: Optional[str] = None
two_factor_authentication: Optional[str] = None,
dxfeed_tos_compliant: bool = False
):
body = {
'login': login,
Expand Down Expand Up @@ -203,8 +206,11 @@ def __init__(
self.validate()

# Pull streamer tokens and urls
url = ('api-quote-tokens'
if dxfeed_tos_compliant
else 'quote-streamer-tokens')
response = requests.get(
f'{self.base_url}/quote-streamer-tokens',
f'{self.base_url}/{url}',
headers=self.headers
)
validate_response(response)
Expand Down
48 changes: 21 additions & 27 deletions tests/test_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,16 @@ def account(session):
return Account.get_accounts(session)[0]


@pytest.fixture(scope='session')
@pytest.fixture
def cert_session(get_cert_credentials):
usr, pwd = get_cert_credentials
session = CertificationSession(usr, pwd)
yield session
session.destroy()


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


def test_get_account(session, account):
Expand Down Expand Up @@ -75,14 +74,14 @@ def test_get_effective_margin_requirements(session, account):

@pytest.fixture(scope='session')
def new_order(session):
symbol = Equity.get_equity(session, 'SPY')
symbol = Equity.get_equity(session, 'NVDA')
leg = symbol.build_leg(Decimal(1), OrderAction.BUY_TO_OPEN)

return NewOrder(
time_in_force=OrderTimeInForce.DAY,
order_type=OrderType.LIMIT,
legs=[leg],
price=Decimal(42), # if this fills the US has crumbled
price=Decimal(10), # if this fills the US has crumbled
price_effect=PriceEffect.DEBIT
)

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

def test_replace_and_delete_order(session, account, new_order, placed_order):
modified_order = new_order.model_copy()
modified_order.price = Decimal(40)
modified_order.price = Decimal(11)
replaced = account.replace_order(session, placed_order.id, modified_order)
sleep(3)
account.delete_order(session, replaced.id)
Expand All @@ -114,25 +113,17 @@ def test_get_order_history(session, account):
account.get_order_history(session, page_offset=0)


def test_get_complex_order_history(session, account):
account.get_complex_order_history(session, page_offset=0)


def test_get_live_orders(session, account):
account.get_live_orders(session)


def test_place_oco_order(cert_session, cert_account):
session = cert_session
account = cert_account
# first, buy share of SPY to set up the OCO order
symbol = Equity.get_equity(session, 'SPY')
opening = symbol.build_leg(Decimal(1), OrderAction.BUY_TO_OPEN)
resp1 = account.place_order(session, NewOrder(
time_in_force=OrderTimeInForce.DAY,
order_type=OrderType.LIMIT,
legs=[opening],
price=Decimal('2.5'), # should fill immediately for cert account
price_effect=PriceEffect.DEBIT
), dry_run=False)
assert resp1.order.status != OrderStatus.REJECTED

def test_place_oco_order(session, account):
# account must have a share of NVDA for this to work
symbol = Equity.get_equity(session, 'NVDA')
closing = symbol.build_leg(Decimal(1), OrderAction.SELL_TO_CLOSE)
oco = NewComplexOrder(
orders=[
Expand All @@ -159,9 +150,7 @@ def test_place_oco_order(cert_session, cert_account):
account.delete_complex_order(session, resp2.complex_order.id)


def test_place_otoco_order(cert_session, cert_account):
session = cert_session
account = cert_account
def test_place_otoco_order(session, account):
symbol = Equity.get_equity(session, 'AAPL')
opening = symbol.build_leg(Decimal(1), OrderAction.BUY_TO_OPEN)
closing = symbol.build_leg(Decimal(1), OrderAction.SELL_TO_CLOSE)
Expand All @@ -170,15 +159,15 @@ def test_place_otoco_order(cert_session, cert_account):
time_in_force=OrderTimeInForce.DAY,
order_type=OrderType.LIMIT,
legs=[opening],
price=Decimal('250'), # won't fill
price=Decimal('100'), # won't fill
price_effect=PriceEffect.DEBIT
),
orders=[
NewOrder(
time_in_force=OrderTimeInForce.GTC,
order_type=OrderType.LIMIT,
legs=[closing],
price=Decimal('2500'), # won't fill
price=Decimal('400'), # won't fill
price_effect=PriceEffect.CREDIT
),
NewOrder(
Expand All @@ -193,3 +182,8 @@ def test_place_otoco_order(cert_session, cert_account):
resp = account.place_complex_order(session, otoco, dry_run=False)
sleep(3)
account.delete_complex_order(session, resp.complex_order.id)


def test_get_live_complex_orders(session, account):
orders = account.get_live_complex_orders(session)
assert orders != []

0 comments on commit b5dc8dd

Please sign in to comment.