Skip to content

Commit

Permalink
#31 Add asyncio support (#99)
Browse files Browse the repository at this point in the history
* Move to HTTPX+authlib (pending PRs merging upstream), add asynchronous support to main client. Also, JSON for creds storage

* use httpx, fix user-agent issue

* update docs

* full test suite for both async and sync clients

* minor renaming, add some docs

* minor renaming, add some docs

* use AsyncMock from stdlib instead of asynctest module

* use authlib-httpx

* improve workflow and messaging

* update test

* fix docs

* add auth lib to repo, adjust utilities for httpx, etc

* stale import

* birthdays have no place here

* update readme

* show how to use async; remove unnecessary print

* no longer consuming this package

* Add note about not using more than one client per token file

* restore missing underscore

* change SyncClient back to Client, move oauth2lib to 3rd-party folder

* add __init__.py to third_party folder

* add notes about why this exists

* make sphinx error on warnings

* fix more underscores

* split client.py
  • Loading branch information
dustydecapod committed Sep 14, 2020
1 parent e4254dd commit 20bbcf7
Show file tree
Hide file tree
Showing 26 changed files with 938 additions and 449 deletions.
2 changes: 1 addition & 1 deletion Makefile.sphinx
Expand Up @@ -3,7 +3,7 @@

# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXOPTS ?=-W
SPHINXBUILD ?= sphinx-build
SOURCEDIR = docs
BUILDDIR = docs-build
Expand Down
2 changes: 1 addition & 1 deletion README.rst
Expand Up @@ -74,7 +74,7 @@ daily historical price data for the past twenty years:
period=client.Client.PriceHistory.Period.TWENTY_YEARS,
frequency_type=client.Client.PriceHistory.FrequencyType.DAILY,
frequency=client.Client.PriceHistory.Frequency.DAILY)
assert r.ok, r.raise_for_status()
assert r.status_code == 200, r.raise_for_status()
print(json.dumps(r.json(), indent=4))
Why should I use ``tda-api``?
Expand Down
42 changes: 40 additions & 2 deletions docs/client.rst
Expand Up @@ -13,6 +13,9 @@ client provides access to all endpoints of the API in as easy and direct a way
as possible. For example, here is how you can fetch the past 20 years of data
for Apple stock:

**Do not attempt to use more than one Client object per token file, as
this will likely cause issues with the underlying OAuth2 session management**

.. code-block:: python
from tda.auth import easy_client
Expand All @@ -28,12 +31,47 @@ for Apple stock:
period=Client.PriceHistory.Period.TWENTY_YEARS,
frequency_type=Client.PriceHistory.FrequencyType.DAILY,
frequency=Client.PriceHistory.Frequency.DAILY)
assert resp.ok
assert resp.status_code == 200
history = resp.json()
Note we we create a new client using the ``auth`` package as described in
:ref:`auth`. Creating a client directly is possible, but not recommended.

+++++++++++++++++++
Asyncio Support
+++++++++++++++++++

An asynchronous variant is available through a keyword to the client
constructor. This allows for higher-performance API usage, at the cost
of slightly increased application complexity.

.. code-block:: python
from tda.auth import easy_client
from tda.client import Client
async def main():
c = easy_client(
api_key='APIKEY',
redirect_uri='https://localhost',
token_path='/tmp/token.pickle',
asyncio=True)
resp = await c.get_price_history('AAPL',
period_type=Client.PriceHistory.PeriodType.YEAR,
period=Client.PriceHistory.Period.TWENTY_YEARS,
frequency_type=Client.PriceHistory.FrequencyType.DAILY,
frequency=Client.PriceHistory.Frequency.DAILY)
assert resp.status_code == 200
history = resp.json()
if __name__ == '__main__':
import asyncio
asyncio.run_until_complete(main())
For more examples, please see the ``examples/async`` directory in
GitHub.

+++++++++++++++++++
Calling Conventions
+++++++++++++++++++
Expand Down Expand Up @@ -67,7 +105,7 @@ users can simply use the following pattern:
.. code-block:: python
r = client.some_endpoint()
assert r.ok, r.raise_for_status()
assert r.status_code == 200, r.raise_for_status()
data = r.json()
The API indicates errors using the response status code, and this pattern will
Expand Down
6 changes: 3 additions & 3 deletions docs/streaming.rst
Expand Up @@ -37,13 +37,13 @@ run this outside regular trading hours you may not see anything):
await stream_client.login()
await stream_client.quality_of_service(StreamClient.QOSLevel.EXPRESS)
await stream_client.nasdaq_book_subs(['GOOG'])
stream_client.add_timesale_options_handler(
stream_client.add_nasdaq_book_handler(
lambda msg: print(json.dumps(msg, indent=4)))
while True:
await stream_client.handle_message()
asyncio.get_event_loop().run_until_complete(read_stream())
asyncio.run(read_stream())
This API uses Python
`coroutines <https://docs.python.org/3/library/asyncio-task.html>`__ to simplify
Expand Down Expand Up @@ -427,7 +427,7 @@ You can identify on which exchange a symbol is listed by using
.. code-block:: python
r = c.search_instruments(['GOOG'], projection=c.Instrument.Projection.FUNDAMENTAL)
assert r.ok, r.raise_for_status()
assert r.status_code == 200, r.raise_for_status()
print(r.json()['GOOG']['exchange']) # Outputs NASDAQ
However, many symbols have order books available on these streams even though
Expand Down
2 changes: 1 addition & 1 deletion docs/util.rst
Expand Up @@ -28,7 +28,7 @@ usage:
# Assume client and order already exist and are valid
account_id = 123456
r = client.place_order(account_id, order)
assert r.ok, raise_for_status()
assert r.status_code == 200, raise_for_status()
order_id = Utils(client, account_id).extract_order_id(r)
assert order_id is not None
Expand Down
36 changes: 36 additions & 0 deletions examples/async/get_quote.py
@@ -0,0 +1,36 @@
from urllib.request import urlopen

import atexit
import datetime
import dateutil
import sys
import tda

API_KEY = 'XXXXXX@AMER.OAUTHAP'
REDIRECT_URI = 'http://localhost:8080/'
TOKEN_PATH = 'ameritrade-credentials.json'


def make_webdriver():
# Import selenium here because it's slow to import
from selenium import webdriver

driver = webdriver.Chrome()
atexit.register(lambda: driver.quit())
return driver


# Create a new client
client = tda.auth.easy_client(
API_KEY,
REDIRECT_URI,
TOKEN_PATH,
make_webdriver, asyncio=True)

async def main():
r = await client.get_quote("AAPL")
print(r.json())

if __name__ == '__main__':
import asyncio
asyncio.run(main())
15 changes: 7 additions & 8 deletions examples/birthday_dividends.py
@@ -1,15 +1,15 @@
from urllib.request import urlopen

import atexit
import datetime
import dateutil
import httpx
import sys
import tda

API_KEY = 'YOUR_API_KEY@AMER.OAUTHAP'
REDIRECT_URI = 'YOUR_REDIRECT_URI'
TOKEN_PATH = '/YOUR/TOKEN/PATH'
API_KEY = 'XXXXXX@AMER.OAUTHAP'
REDIRECT_URI = 'https://localhost:8080/'
TOKEN_PATH = 'ameritrade-credentials.json'
YOUR_BIRTHDAY = datetime.datetime(year=1969, month=4, day=20)
SP500_URL = "https://tda-api.readthedocs.io/en/latest/_static/sp500.txt"


def make_webdriver():
Expand All @@ -29,8 +29,7 @@ def make_webdriver():
make_webdriver)

# Load S&P 500 composition from documentation
sp500 = urlopen(
'https://tda-api.readthedocs.io/en/latest/_static/sp500.txt').read().decode().split()
sp500 = httpx.get(SP500_URL, headers={"User-Agent":"Mozilla/5.0"}).read().decode().split()

# Fetch fundamentals for all symbols and filter out the ones with ex-dividend
# dates in the future and dividend payment dates on your birth month. Note we
Expand All @@ -41,7 +40,7 @@ def make_webdriver():
for s in (sp500[:250], sp500[250:]):
r = client.search_instruments(
s, tda.client.Client.Instrument.Projection.FUNDAMENTAL)
assert r.ok, r.raise_for_status()
assert r.status_code == 200, r.raise_for_status()

for symbol, f in r.json().items():

Expand Down
31 changes: 31 additions & 0 deletions examples/get_quote.py
@@ -0,0 +1,31 @@
from urllib.request import urlopen

import atexit
import datetime
import dateutil
import sys
import tda

API_KEY = 'XXXXXX@AMER.OAUTHAP'
REDIRECT_URI = 'http://localhost:8080/'
TOKEN_PATH = 'ameritrade-credentials.json'

def make_webdriver():
# Import selenium here because it's slow to import
from selenium import webdriver

driver = webdriver.Chrome()
atexit.register(lambda: driver.quit())
return driver


# Create a new client
client = tda.auth.easy_client(
API_KEY,
REDIRECT_URI,
TOKEN_PATH,
make_webdriver)


r = client.get_quote("AAPL")
print(r.json())
4 changes: 3 additions & 1 deletion requirements.txt
@@ -1,7 +1,9 @@
asynctest
colorama
coverage
requests_oauthlib
tox
authlib==0.14.3
httpx==0.14.3
python-dateutil
pytest
pytz
Expand Down
3 changes: 2 additions & 1 deletion setup.py
Expand Up @@ -30,7 +30,8 @@
],
python_requires='>=3.6',
install_requires=[
'requests_oauthlib',
'authlib==0.14.3',
'httpx==0.14.3',
'python-dateutil',
'selenium',
'websockets'],
Expand Down

0 comments on commit 20bbcf7

Please sign in to comment.