Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add transaction history pagination support #56

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 48 additions & 17 deletions tastyworks/example.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import asyncio
import calendar
import logging

from os import environ
from datetime import date, timedelta
from decimal import Decimal
Expand All @@ -15,6 +16,7 @@
from tastyworks.streamer import DataStreamer
from tastyworks.tastyworks_api import tasty_session


LOGGER = logging.getLogger(__name__)


Expand All @@ -29,23 +31,47 @@ async def main_loop(session: TastyAPISession, streamer: DataStreamer):
"Quote": ["/ES"]
}

accounts = await TradingAccount.get_remote_accounts(session)
acct = accounts[0]
LOGGER.info('Accounts available: %s', accounts)
accounts = await TradingAccount.get_accounts(session)
account_nums = list(map(lambda x: x.account_number, accounts))
print(f'Accounts available: {account_nums}')

for acct in accounts:
anum = acct.account_number
bal = await acct.get_balance()
print(f'--- Account {anum} Balances ---')
print(bal)

print(f'--- Account {anum} Positions ---')
positions = await acct.get_positions()
for pos in positions:
print(f'{pos["symbol"]:30s} {pos["quantity"]:3d} {pos["average-open-price"]:8s} {pos["close-price"]}')

print(f'--- Account {anum} Transaction History ---')
history = await acct.get_history()
for h in history:
# print(f'{h["executed-at"]:20s} {h["action"]:15s} {h["underlying-symbol"]:6s} {h["quantity"]} {h["price"]}')
print(f'{h["executed-at"]:32s} {h["description"]}')

orders = await Order.get_remote_orders(session, acct)
LOGGER.info('Number of active orders: %s', len(orders))
print(f'Number of active orders: {len(orders)}')

await streamer.add_data_sub(sub_values)

print('Monitoring /ES updates...')
async for item in streamer.listen():
LOGGER.info('Received item: %s' % item.data)

# Execute an order

async def execute_order(acct, session):
# Execute an order
details = OrderDetails(
type=OrderType.LIMIT,
price=Decimal(400),
price_effect=OrderPriceEffect.CREDIT)
new_order = Order(details)

opt = Option(
ticker='AKS',
ticker='F',
quantity=1,
expiry=get_third_friday(date.today()),
strike=Decimal(3),
Expand All @@ -57,16 +83,15 @@ async def main_loop(session: TastyAPISession, streamer: DataStreamer):
res = await acct.execute_order(new_order, session, dry_run=True)
LOGGER.info('Order executed successfully: %s', res)

# Get an options chain
undl = underlying.Underlying('AKS')

chain = await option_chain.get_option_chain(session, undl)
LOGGER.info('Chain strikes: %s', chain.get_all_strikes())

await streamer.add_data_sub(sub_values)

async for item in streamer.listen():
LOGGER.info('Received item: %s' % item.data)
async def get_options_chain(symbol, session):
# Get an options chain
try:
undl = underlying.Underlying(symbol)
chain = await option_chain.get_option_chain(session, undl)
LOGGER.info('Chain strikes: %s', chain.get_all_strikes())
except Exception:
LOGGER.error(f'Could not get options for {symbol}')


def get_third_friday(d):
Expand All @@ -83,7 +108,13 @@ def get_third_friday(d):


def main():
tasty_client = tasty_session.create_new_session(environ.get('TW_USER', ""), environ.get('TW_PASSWORD', ""))
user = environ.get('TW_USER', '')
password = environ.get('TW_PASSWORD', '')
if user == '':
LOGGER.exception('Please provide username')
if password == '':
LOGGER.exception('Please provide password')
tasty_client = tasty_session.create_new_session(user, password)

streamer = DataStreamer(tasty_client)
LOGGER.info('Streamer token: %s' % streamer.get_streamer_token())
Expand All @@ -96,7 +127,7 @@ def main():
finally:
# find all futures/tasks still running and wait for them to finish
pending_tasks = [
task for task in asyncio.Task.all_tasks() if not task.done()
task for task in asyncio.all_tasks() if not task.done()
]
loop.run_until_complete(asyncio.gather(*pending_tasks))
loop.close()
Expand Down
5 changes: 4 additions & 1 deletion tastyworks/models/option_chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,7 @@ async def _get_tasty_option_chain_data(session, underlying) -> Dict:
resp = await response.json()

# NOTE: Have not seen an example with more than 1 item. No idea what that would be.
return resp['data']['items'][0]
if 'items' in resp['data'] and len(resp['data']['items']) > 0:
return resp['data']['items'][0]
else:
raise Exception(f'Empty option chain data for symbol {underlying.ticker}')
101 changes: 54 additions & 47 deletions tastyworks/models/trading_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,22 @@
from dataclasses import dataclass

from tastyworks.models.order import Order, OrderPriceEffect
from tastyworks.models.session import TastyAPISession


@dataclass
class TradingAccount(object):
account_number: str
external_id: str
is_margin: bool
session: TastyAPISession

async def execute_order(self, order: Order, session, dry_run=True):
async def execute_order(self, order: Order, dry_run=True):
"""
Execute an order. If doing a dry run, the order isn't placed but simulated (server-side).

Args:
order (Order): The order object to execute.
session (TastyAPISession): The tastyworks session onto which to execute the order.
dry_run (bool): Whether to do a test (dry) run.

Returns:
Expand All @@ -27,19 +28,19 @@ async def execute_order(self, order: Order, session, dry_run=True):
if not order.check_is_order_executable():
raise Exception('Order is not executable, most likely due to missing data')

if not session.is_active():
raise Exception('The supplied session is not active and valid')
if not self.session.is_active():
raise Exception('The session is not active or valid')

url = '{}/accounts/{}/orders'.format(
session.API_url,
self.session.API_url,
self.account_number
)
if dry_run:
url = f'{url}/dry-run'

body = _get_execute_order_json(order)

async with aiohttp.request('POST', url, headers=session.get_request_headers(), json=body) as resp:
async with aiohttp.request('POST', url, headers=self.session.get_request_headers(), json=body) as resp:
if resp.status == 201:
return True
elif resp.status == 400:
Expand All @@ -48,22 +49,23 @@ async def execute_order(self, order: Order, session, dry_run=True):
raise Exception('Unknown remote error, status code: {}, message: {}'.format(resp.status, await resp.text()))

@classmethod
def from_dict(cls, data: dict) -> List:
def from_dict(cls, data: dict, session: TastyAPISession) -> List:
"""
Parses a TradingAccount object from a dict.
"""
new_data = {
'is_margin': True if data['margin-or-cash'] == 'Margin' else False,
'account_number': data['account-number'],
'external_id': data['external-id']
'external_id': data['external-id'],
'session': session
}

res = TradingAccount(**new_data)

return res

@classmethod
async def get_remote_accounts(cls, session) -> List:
async def get_accounts(cls, session) -> List:
"""
Gets all trading accounts from the Tastyworks platform.

Expand All @@ -78,101 +80,106 @@ async def get_remote_accounts(cls, session) -> List:

async with aiohttp.request('GET', url, headers=session.get_request_headers()) as response:
if response.status != 200:
raise Exception('Could not get trading accounts info from Tastyworks...')
raise Exception(f'HTTP {response.status} during GET accounts')
data = (await response.json())['data']

for entry in data['items']:
if entry['authority-level'] != 'owner':
continue
acct_data = entry['account']
acct = TradingAccount.from_dict(acct_data)
acct = TradingAccount.from_dict(acct_data, session)
res.append(acct)

return res

async def get_balance(session, account):
async def get_balance(self):
"""
Get balance.

Args:
session (TastyAPISession): An active and logged-in session object against which to query.
account (TradingAccount): The account_id to get balance on.
account (TradingAccount): The account to get balance on.
Returns:
dict: account attributes
"""
url = '{}/accounts/{}/balances'.format(
session.API_url,
account.account_number
self.session.API_url,
self.account_number
)

async with aiohttp.request('GET', url, headers=session.get_request_headers()) as response:
async with aiohttp.request('GET', url, headers=self.session.get_request_headers()) as response:
if response.status != 200:
raise Exception('Could not get trading account balance info from Tastyworks...')
raise Exception(f'HTTP {response.status} during GET balances from {self.account_number}')
data = (await response.json())['data']
return data

async def get_positions(session, account):
async def get_positions(self):
"""
Get Open Positions.

Args:
session (TastyAPISession): An active and logged-in session object against which to query.
account (TradingAccount): The account_id to get positions on.
account (TradingAccount): The account to get positions on.
Returns:
dict: account attributes
"""
url = '{}/accounts/{}/positions'.format(
session.API_url,
account.account_number
self.session.API_url,
self.account_number
)

async with aiohttp.request('GET', url, headers=session.get_request_headers()) as response:
async with aiohttp.request('GET', url, headers=self.session.get_request_headers()) as response:
if response.status != 200:
raise Exception('Could not get open positions info from Tastyworks...')
raise Exception(f'HTTP {response.status} during GET positions from {self.account_number}')
data = (await response.json())['data']['items']
return data

async def get_live_orders(session, account):
async def get_live_orders(self):
"""
Get live Orders.
Get live orders from the account

Args:
session (TastyAPISession): An active and logged-in session object against which to query.
account (TradingAccount): The account_id to get live orders on.
None
Returns:
dict: account attributes
"""
url = '{}/accounts/{}/orders/live'.format(
session.API_url,
account.account_number
self.session.API_url,
self.account_number
)

async with aiohttp.request('GET', url, headers=session.get_request_headers()) as response:
async with aiohttp.request('GET', url, headers=self.session.get_request_headers()) as response:
if response.status != 200:
raise Exception('Could not get live orders info from Tastyworks...')
raise Exception(f'HTTP {response.status} during GET orders/live')
data = (await response.json())['data']['items']
return data

async def get_history(session, account):
async def get_history(self):
"""
Get live Orders.
Get transaction history from the account

Args:
session (TastyAPISession): An active and logged-in session object against which to query.
account (TradingAccount): The account_id to get history on.
None
Returns:
dict: account attributes
"""
url = '{}/accounts/{}/transactions'.format(
session.API_url,
account.account_number
)

async with aiohttp.request('GET', url, headers=session.get_request_headers()) as response:
if response.status != 200:
raise Exception('Could not get history info from Tastyworks...')
data = (await response.json())['data']
return data
total_pages = page = 0
history = []
while True:
url = '{}/accounts/{}/transactions?start-at=2020-01-01&end-at=2022-12-31&per-page=100&page-offset={}'.format(
self.session.API_url,
self.account_number,
page
)
async with aiohttp.request('GET', url, headers=self.session.get_request_headers()) as response:
if response.status != 200:
raise Exception(f'HTTP {response.status} during GET transactions')
tmp = (await response.json())
if 'pagination' in tmp.keys():
total_pages = tmp['pagination']['total-pages']
history.extend(tmp['data']['items'])
page += 1
if page == total_pages:
break
return history


def _get_execute_order_json(order: Order):
Expand Down