Skip to content

Commit 0f22aa4

Browse files
committed
add auto-refresh sessions, test suite
1 parent 406b057 commit 0f22aa4

19 files changed

+1025
-55
lines changed

.github/CONTRIBUTING.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Contributions
2+
3+
In order to run the test suite locally, you'll need to follow these steps to be able to authenticate to Tradestation.
4+
5+
## Steps to follow to contribute
6+
7+
1. Fork the repository to your personal Github account and make your proposed changes.
8+
2. Export your API key, secret key, refresh token, and account number to the following Github Actions repository secrets: `TS_API_KEY`, `TS_SECRET_KEY`, `TS_REFRESH`, and `TS_ACCOUNT`. The account should be a simulation account.
9+
3. Run `make install` to create the virtual environment, `make lint` to format your code, and `make test` to run the tests locally.

.github/pull_request_template.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,7 @@ Fixes ...
66
## Pre-merge checklist
77
- [ ] Code formatted correctly (check with `make lint`)
88
- [ ] Code implemented for both sync and async
9-
- [ ] Passing tests locally (check with `make test`)
9+
- [ ] Passing tests locally (check with `make test`, make sure you have `TS_API_KEY`, `TS_SECRET_KEY`, `TS_REFRESH`, and `TS_ACCOUNT` environment variables set)
1010
- [ ] New tests added (if applicable)
11+
12+
Please note that, in order to pass the tests, you'll need to set up your Tradestation credentials as repository secrets on your local fork. Read more at CONTRIBUTING.md.

docs/conf.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
#
33
# For the full list of built-in configuration values, see the documentation:
44
# https://www.sphinx-doc.org/en/master/usage/configuration.html
5+
import os
6+
import sys
7+
8+
sys.path.insert(0, os.path.abspath(".."))
59

610
# -- Project information -----------------------------------------------------
711
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
@@ -14,7 +18,22 @@
1418
# -- General configuration ---------------------------------------------------
1519
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
1620

17-
extensions = ["sphinx_rtd_theme"]
21+
extensions = [
22+
"sphinx.ext.duration",
23+
"sphinx.ext.doctest",
24+
"sphinx.ext.autodoc",
25+
"sphinx.ext.autosummary",
26+
"sphinx.ext.intersphinx",
27+
"enum_tools.autoenum",
28+
"sphinxcontrib.autodoc_pydantic",
29+
]
30+
31+
intersphinx_mapping = {
32+
"rtd": ("https://docs.readthedocs.io/en/stable/", None),
33+
"python": ("https://docs.python.org/3/", None),
34+
"sphinx": ("https://www.sphinx-doc.org/en/master/", None),
35+
}
36+
intersphinx_disabled_domains = ["std"]
1837

1938
templates_path = ["_templates"]
2039
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
@@ -25,3 +44,11 @@
2544

2645
html_theme = "sphinx_rtd_theme"
2746
html_static_path = ["_static"]
47+
48+
# -- Options for pydantic -------------------------------------------------
49+
autodoc_pydantic_model_show_json = True
50+
autodoc_pydantic_settings_show_json = False
51+
autodoc_pydantic_show_field_summary = True
52+
autodoc_pydantic_model_undoc_members = False
53+
autodoc_pydantic_model_hide_paramlist = False
54+
autodoc_pydantic_model_show_config_summary = False

docs/index.rst

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,44 @@
1-
.. tradestation documentation master file, created by
2-
sphinx-quickstart on Mon Jan 13 22:34:26 2025.
3-
You can adapt this file completely to your liking, but it should at least
4-
contain the root `toctree` directive.
1+
.. image:: https://readthedocs.org/projects/tradestation/badge/?version=latest
2+
:target: https://tradestation.readthedocs.io/en/latest/?badge=latest
3+
:alt: Documentation Status
54

6-
tradestation documentation
7-
==========================
5+
.. image:: https://img.shields.io/pypi/v/tradestation
6+
:target: https://pypi.org/project/tradestation
7+
:alt: PyPI Package
88

9-
Add your content using ``reStructuredText`` syntax. See the
10-
`reStructuredText <https://www.sphinx-doc.org/en/master/usage/restructuredtext/index.html>`_
11-
documentation for details.
9+
.. image:: https://static.pepy.tech/badge/tradestation
10+
:target: https://pepy.tech/project/tradestation
11+
:alt: PyPI Downloads
1212

13+
.. image:: https://img.shields.io/github/v/release/tastyware/tradestation?label=release%20notes
14+
:target: https://github.com/tastyware/tradestation/releases
15+
:alt: Release
16+
17+
TradeStation Python SDK
18+
=======================
19+
20+
A simple, sync/async SDK for TradeStation built on their public API. This will allow you to create trading algorithms for whatever strategies you may have quickly and painlessly in Python.
21+
22+
.. tip::
23+
Do you use Tastytrade? Our `Tastytrade SDK <https://github.com/tastyware/tastytrade>`_ has many of the same features!
1324

1425
.. toctree::
1526
:maxdepth: 2
16-
:caption: Contents:
27+
:caption: Documentation:
28+
29+
installation
30+
sessions
31+
sync-async
32+
33+
.. toctree::
34+
:maxdepth: 2
35+
:caption: SDK Reference:
36+
37+
tradestation
38+
39+
Indices and tables
40+
==================
1741

42+
* :ref:`genindex`
43+
* :ref:`modindex`
44+
* :ref:`search`

docs/installation.rst

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
Installation
2+
============
3+
4+
Via pypi
5+
--------
6+
7+
The easiest way to install the SDK is using pip:
8+
9+
::
10+
11+
$ pip install tradestation
12+
13+
From source
14+
-----------
15+
16+
You can also install from source.
17+
Make sure you have `uv <https://docs.astral.sh/uv/getting-started/installation/>`_ installed beforehand.
18+
19+
::
20+
21+
$ git clone https://github.com/tastyware/tradestation.git
22+
$ cd tradestation
23+
$ make install
24+
25+
If you're contributing, you'll want to run tests on your changes locally:
26+
27+
::
28+
29+
$ make lint
30+
$ make test
31+
32+
If you want to build the documentation (usually not necessary):
33+
34+
::
35+
36+
$ make docs
37+
38+
Windows
39+
-------
40+
41+
If you want to install from source on Windows, you can't use the Makefile, so just run the commands individually. For example:
42+
43+
::
44+
45+
$ git clone https://github.com/tastyware/tradestation.git
46+
$ cd tradestation
47+
$ uv sync
48+
$ uv pip install -e .

docs/sessions.rst

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
Sessions
2+
========
3+
4+
Initial setup
5+
-------------
6+
7+
Tradestation uses OAuth for secure authentication to the API. In order to obtain access tokens, you need to authenticate with OAuth 2's authorization code flow, which requires a local HTTP server running to handle the callback. Fortunately, the SDK makes doing this easy:
8+
9+
.. code-block:: python
10+
11+
from tradestation.oauth import login
12+
login()
13+
14+
This will let you authenticate in your local browser. Fortunately, this only needs to be done once, as afterwards you can use the refresh token to obtain new access tokens indefinitely.
15+
16+
Creating a session
17+
------------------
18+
19+
A session object is required to authenticate your requests to the Tradestation API.
20+
Create it by passing in the API key (client ID), secret key (client secret), and refresh token obtained in the initial setup.
21+
22+
.. code-block:: python
23+
24+
from tradestation import Session
25+
session = Session('api_key', 'secret_key', 'refresh_token')
26+
27+
A simulated session can be used to test strategies or applications before using them in production:
28+
29+
.. code-block:: python
30+
31+
from tradestation import Session
32+
session = Session('api_key', 'secret_key', 'refresh_token', is_test=True)
33+
34+
You can also use the legacy v2 API endpoints if desired:
35+
36+
.. code-block:: python
37+
38+
from tradestation import Session
39+
session = Session('api_key', 'secret_key', 'refresh_token', use_v2=True)
40+
41+
Auto-refresh sessions
42+
---------------------
43+
44+
Since TradeStation access tokens only last 20 minutes by default, it can be annoying to have to remember to refresh them constantly.
45+
Fortunately, the SDK has a special class, `AutoRefreshSession`, that handles token refreshal (ok, ok, I know it's not a word!) for you!
46+
47+
.. code-block:: python
48+
49+
from tradestation import AutoRefreshSession
50+
session = await AutoRefreshSession('api_key', 'secret_key', 'refresh_token', is_test=True)
51+
# ...
52+
await session.close() # don't forget to cleanup the session when you're done!
53+
54+
You can also create auto-refresh sessions using async context managers:
55+
56+
.. code-block:: python
57+
58+
async with AutoRefreshSession('api_key', 'secret_key', 'refresh_token') as session:
59+
# ...
60+
61+
In this case, the context manager will handle the cleanup for you.
62+
Pretty easy, no? Other than initialization and cleanup, you can use auto-refresh sessions just like normal sessions.

docs/sync-async.rst

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
sync/async
2+
==========
3+
4+
After creating a session (which is always initialized synchronously), the rest of the API endpoints implemented in the SDK have both sync and async implementations.
5+
6+
Let's see how this looks:
7+
8+
.. code-block:: python
9+
10+
from tradestation import Account, Session
11+
session = Session('api_key', 'secret_key', 'refresh_token')
12+
accounts = Account.get_accounts(session)
13+
14+
The async implementation is similar:
15+
16+
.. code-block:: python
17+
18+
session = Session('api_key', 'secret_key', 'refresh_token')
19+
# using async implementation
20+
accounts = await Account.a_get_accounts(session)
21+
22+
That's it! All sync methods have a parallel async method that starts with `a_`.

docs/tradestation.rst

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
tradestation
2+
============
3+
4+
Account
5+
-------
6+
.. automodule:: tradestation.account
7+
:members:
8+
:show-inheritance:
9+
10+
Session
11+
-------
12+
.. automodule:: tradestation.session
13+
:members:
14+
:show-inheritance:
15+
16+
Utils
17+
-----
18+
.. automodule:: tradestation.utils
19+
:members:
20+
:inherited-members:

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ dev-dependencies = [
3232
"pytest-aio>=1.9.0",
3333
"sphinx>=8.1.3",
3434
"sphinx-rtd-theme>=3.0.2",
35+
"autodoc-pydantic>=2.2.0",
36+
"enum-tools[sphinx]>=0.12.0",
3537
]
3638

3739
[tool.setuptools.package-data]

tests/conftest.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
from typing import AsyncGenerator
23

34
from pytest import fixture
45

@@ -7,21 +8,24 @@
78

89
# Run all tests with asyncio only
910
@fixture(scope="session")
10-
def aiolib():
11+
def aiolib() -> str:
1112
return "asyncio"
1213

1314

1415
@fixture(scope="session")
15-
def credentials():
16-
username = os.getenv("TS_USERNAME")
17-
password = os.getenv("TS_PASSWORD")
18-
assert username is not None
19-
assert password is not None
20-
return username, password
16+
def credentials() -> tuple[str, str, str]:
17+
api_key = os.getenv("TS_API_KEY")
18+
secret_key = os.getenv("TS_SECRET_KEY")
19+
refresh_token = os.getenv("TS_REFRESH")
20+
assert api_key is not None
21+
assert secret_key is not None
22+
assert refresh_token is not None
23+
return api_key, secret_key, refresh_token
2124

2225

2326
@fixture(scope="session")
24-
async def session(credentials, aiolib):
25-
session = Session(*credentials)
27+
async def session(
28+
credentials: tuple[str, str, str], aiolib: str
29+
) -> AsyncGenerator[Session, None]:
30+
session = Session(*credentials, is_test=True)
2631
yield session
27-
# session.destroy()

tests/test_account.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from pytest import fixture
2+
3+
from tradestation import Account, Session
4+
5+
6+
@fixture(scope="module")
7+
def accounts(session: Session) -> list[Account]:
8+
return Account.get_accounts(session)
9+
10+
11+
def test_get_accounts(accounts: list[Account]):
12+
assert accounts != []
13+
14+
15+
async def test_get_accounts_async(session: Session):
16+
accounts = await Account.a_get_accounts(session)
17+
assert accounts != []
18+
19+
20+
def test_get_balances(session: Session, accounts: list[Account]):
21+
balances = Account.get_balances(session, accounts)
22+
assert balances.balances != []
23+
24+
25+
async def test_get_balances_async(session: Session, accounts: list[Account]):
26+
balances = await Account.a_get_balances(session, accounts)
27+
assert balances.balances != []

tests/test_oauth.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import pytest
2+
import signal
3+
4+
from tradestation.oauth import (
5+
Credentials,
6+
get_access_url,
7+
convert_auth_code,
8+
login,
9+
response_page,
10+
)
11+
12+
13+
def test_get_access_url():
14+
credentials = Credentials(key="test")
15+
url = get_access_url(credentials)
16+
assert "test" in url
17+
18+
19+
def test_convert_auth_code():
20+
with pytest.raises(Exception):
21+
convert_auth_code(Credentials(), "bogus")
22+
23+
24+
def test_response_page():
25+
page = response_page("refresh", "access", {"key": "value"})
26+
assert isinstance(page, bytes)
27+
28+
29+
def handler(signum, frame):
30+
raise TimeoutError
31+
32+
33+
def test_login():
34+
signal.signal(signal.SIGALRM, handler)
35+
signal.alarm(3)
36+
with pytest.raises(TimeoutError):
37+
login()

0 commit comments

Comments
 (0)