Skip to content

Commit

Permalink
add auto-refresh sessions, test suite
Browse files Browse the repository at this point in the history
  • Loading branch information
Graeme22 committed Jan 14, 2025
1 parent 406b057 commit 0f22aa4
Show file tree
Hide file tree
Showing 19 changed files with 1,025 additions and 55 deletions.
9 changes: 9 additions & 0 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Contributions

In order to run the test suite locally, you'll need to follow these steps to be able to authenticate to Tradestation.

## Steps to follow to contribute

1. Fork the repository to your personal Github account and make your proposed changes.
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.
3. Run `make install` to create the virtual environment, `make lint` to format your code, and `make test` to run the tests locally.
4 changes: 3 additions & 1 deletion .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@ Fixes ...
## Pre-merge checklist
- [ ] Code formatted correctly (check with `make lint`)
- [ ] Code implemented for both sync and async
- [ ] Passing tests locally (check with `make test`)
- [ ] 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)
- [ ] New tests added (if applicable)

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.
29 changes: 28 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
import os
import sys

sys.path.insert(0, os.path.abspath(".."))

# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
Expand All @@ -14,7 +18,22 @@
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration

extensions = ["sphinx_rtd_theme"]
extensions = [
"sphinx.ext.duration",
"sphinx.ext.doctest",
"sphinx.ext.autodoc",
"sphinx.ext.autosummary",
"sphinx.ext.intersphinx",
"enum_tools.autoenum",
"sphinxcontrib.autodoc_pydantic",
]

intersphinx_mapping = {
"rtd": ("https://docs.readthedocs.io/en/stable/", None),
"python": ("https://docs.python.org/3/", None),
"sphinx": ("https://www.sphinx-doc.org/en/master/", None),
}
intersphinx_disabled_domains = ["std"]

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

html_theme = "sphinx_rtd_theme"
html_static_path = ["_static"]

# -- Options for pydantic -------------------------------------------------
autodoc_pydantic_model_show_json = True
autodoc_pydantic_settings_show_json = False
autodoc_pydantic_show_field_summary = True
autodoc_pydantic_model_undoc_members = False
autodoc_pydantic_model_hide_paramlist = False
autodoc_pydantic_model_show_config_summary = False
47 changes: 37 additions & 10 deletions docs/index.rst
Original file line number Diff line number Diff line change
@@ -1,17 +1,44 @@
.. tradestation documentation master file, created by
sphinx-quickstart on Mon Jan 13 22:34:26 2025.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
.. image:: https://readthedocs.org/projects/tradestation/badge/?version=latest
:target: https://tradestation.readthedocs.io/en/latest/?badge=latest
:alt: Documentation Status

tradestation documentation
==========================
.. image:: https://img.shields.io/pypi/v/tradestation
:target: https://pypi.org/project/tradestation
:alt: PyPI Package

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

.. image:: https://img.shields.io/github/v/release/tastyware/tradestation?label=release%20notes
:target: https://github.com/tastyware/tradestation/releases
:alt: Release

TradeStation Python SDK
=======================

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.

.. tip::
Do you use Tastytrade? Our `Tastytrade SDK <https://github.com/tastyware/tastytrade>`_ has many of the same features!

.. toctree::
:maxdepth: 2
:caption: Contents:
:caption: Documentation:

installation
sessions
sync-async

.. toctree::
:maxdepth: 2
:caption: SDK Reference:

tradestation

Indices and tables
==================

* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
48 changes: 48 additions & 0 deletions docs/installation.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
Installation
============

Via pypi
--------

The easiest way to install the SDK is using pip:

::

$ pip install tradestation

From source
-----------

You can also install from source.
Make sure you have `uv <https://docs.astral.sh/uv/getting-started/installation/>`_ installed beforehand.

::

$ git clone https://github.com/tastyware/tradestation.git
$ cd tradestation
$ make install

If you're contributing, you'll want to run tests on your changes locally:

::

$ make lint
$ make test

If you want to build the documentation (usually not necessary):

::

$ make docs

Windows
-------

If you want to install from source on Windows, you can't use the Makefile, so just run the commands individually. For example:

::

$ git clone https://github.com/tastyware/tradestation.git
$ cd tradestation
$ uv sync
$ uv pip install -e .
62 changes: 62 additions & 0 deletions docs/sessions.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
Sessions
========

Initial setup
-------------

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:

.. code-block:: python
from tradestation.oauth import login
login()
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.

Creating a session
------------------

A session object is required to authenticate your requests to the Tradestation API.
Create it by passing in the API key (client ID), secret key (client secret), and refresh token obtained in the initial setup.

.. code-block:: python
from tradestation import Session
session = Session('api_key', 'secret_key', 'refresh_token')
A simulated session can be used to test strategies or applications before using them in production:

.. code-block:: python
from tradestation import Session
session = Session('api_key', 'secret_key', 'refresh_token', is_test=True)
You can also use the legacy v2 API endpoints if desired:

.. code-block:: python
from tradestation import Session
session = Session('api_key', 'secret_key', 'refresh_token', use_v2=True)
Auto-refresh sessions
---------------------

Since TradeStation access tokens only last 20 minutes by default, it can be annoying to have to remember to refresh them constantly.
Fortunately, the SDK has a special class, `AutoRefreshSession`, that handles token refreshal (ok, ok, I know it's not a word!) for you!

.. code-block:: python
from tradestation import AutoRefreshSession
session = await AutoRefreshSession('api_key', 'secret_key', 'refresh_token', is_test=True)
# ...
await session.close() # don't forget to cleanup the session when you're done!
You can also create auto-refresh sessions using async context managers:

.. code-block:: python
async with AutoRefreshSession('api_key', 'secret_key', 'refresh_token') as session:
# ...
In this case, the context manager will handle the cleanup for you.
Pretty easy, no? Other than initialization and cleanup, you can use auto-refresh sessions just like normal sessions.
22 changes: 22 additions & 0 deletions docs/sync-async.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
sync/async
==========

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.

Let's see how this looks:

.. code-block:: python
from tradestation import Account, Session
session = Session('api_key', 'secret_key', 'refresh_token')
accounts = Account.get_accounts(session)
The async implementation is similar:

.. code-block:: python
session = Session('api_key', 'secret_key', 'refresh_token')
# using async implementation
accounts = await Account.a_get_accounts(session)
That's it! All sync methods have a parallel async method that starts with `a_`.
20 changes: 20 additions & 0 deletions docs/tradestation.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
tradestation
============

Account
-------
.. automodule:: tradestation.account
:members:
:show-inheritance:

Session
-------
.. automodule:: tradestation.session
:members:
:show-inheritance:

Utils
-----
.. automodule:: tradestation.utils
:members:
:inherited-members:
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ dev-dependencies = [
"pytest-aio>=1.9.0",
"sphinx>=8.1.3",
"sphinx-rtd-theme>=3.0.2",
"autodoc-pydantic>=2.2.0",
"enum-tools[sphinx]>=0.12.0",
]

[tool.setuptools.package-data]
Expand Down
24 changes: 14 additions & 10 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
from typing import AsyncGenerator

from pytest import fixture

Expand All @@ -7,21 +8,24 @@

# Run all tests with asyncio only
@fixture(scope="session")
def aiolib():
def aiolib() -> str:
return "asyncio"


@fixture(scope="session")
def credentials():
username = os.getenv("TS_USERNAME")
password = os.getenv("TS_PASSWORD")
assert username is not None
assert password is not None
return username, password
def credentials() -> tuple[str, str, str]:
api_key = os.getenv("TS_API_KEY")
secret_key = os.getenv("TS_SECRET_KEY")
refresh_token = os.getenv("TS_REFRESH")
assert api_key is not None
assert secret_key is not None
assert refresh_token is not None
return api_key, secret_key, refresh_token


@fixture(scope="session")
async def session(credentials, aiolib):
session = Session(*credentials)
async def session(
credentials: tuple[str, str, str], aiolib: str
) -> AsyncGenerator[Session, None]:
session = Session(*credentials, is_test=True)
yield session
# session.destroy()
27 changes: 27 additions & 0 deletions tests/test_account.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from pytest import fixture

from tradestation import Account, Session


@fixture(scope="module")
def accounts(session: Session) -> list[Account]:
return Account.get_accounts(session)


def test_get_accounts(accounts: list[Account]):
assert accounts != []


async def test_get_accounts_async(session: Session):
accounts = await Account.a_get_accounts(session)
assert accounts != []


def test_get_balances(session: Session, accounts: list[Account]):
balances = Account.get_balances(session, accounts)
assert balances.balances != []


async def test_get_balances_async(session: Session, accounts: list[Account]):
balances = await Account.a_get_balances(session, accounts)
assert balances.balances != []
37 changes: 37 additions & 0 deletions tests/test_oauth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import pytest
import signal

from tradestation.oauth import (
Credentials,
get_access_url,
convert_auth_code,
login,
response_page,
)


def test_get_access_url():
credentials = Credentials(key="test")
url = get_access_url(credentials)
assert "test" in url


def test_convert_auth_code():
with pytest.raises(Exception):
convert_auth_code(Credentials(), "bogus")


def test_response_page():
page = response_page("refresh", "access", {"key": "value"})
assert isinstance(page, bytes)


def handler(signum, frame):
raise TimeoutError


def test_login():
signal.signal(signal.SIGALRM, handler)
signal.alarm(3)
with pytest.raises(TimeoutError):
login()
Loading

0 comments on commit 0f22aa4

Please sign in to comment.