15 |
16 |
17 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | SPHINXPROJ = pyTD
8 | SOURCEDIR = source
9 | BUILDDIR = build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 |
16 | .PHONY: help Makefile
17 |
18 | # Catch-all target: route all unknown targets to Sphinx using the new
19 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
20 | %: Makefile
21 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
22 |
23 | livehtml:
24 | sphinx-autobuild source build --host 127.0.0.1 -p 8001
--------------------------------------------------------------------------------
/scripts/test.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 |
3 | # Obtain refresh token and store
4 | A=$(python get_refresh_token.py)
5 | export TD_REFRESH_TOKEN=$A
6 |
7 | cd ..
8 |
9 | # flake8 check
10 | echo "flake8 check..."
11 | flake8 pyTD
12 | rc=$?; if [[ $rc != 0 ]]; then
13 | echo "flake8 check failed."
14 | exit $rc;
15 | fi
16 | echo "PASSED"
17 |
18 | # flake8-rst check
19 | echo "flake8-rst docs check..."
20 | flake8-rst --filename="*.rst" .
21 | rc=$?; if [[ $rc != 0 ]]; then
22 | echo "flake8-rst docs check failed."
23 | exit $rc;
24 | fi
25 | echo "PASSED"
26 |
27 | # run all tests
28 | echo "pytest..."
29 | cd pyTD
30 | pytest -x tests
31 | rc=$?;
32 |
33 | if [[ $rc != 0 ]]; then
34 | echo "Pytest failed."
35 | exit $rc
36 | fi
37 | echo "PASSED"
38 |
39 | echo 'All tests passed!'
40 |
--------------------------------------------------------------------------------
/pyTD/auth/_static/style.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css?family=Montserrat');
2 | h1{font-family: "montserrat"; font-size=18px; font-weight: bold;}
3 |
4 | html,
5 | body {
6 | height: 100%;
7 | background-color: #b3b3b3;
8 | }
9 |
10 | body {
11 | display: -ms-flexbox;
12 | display: -webkit-box;
13 | display: flex;
14 | -ms-flex-align: center;
15 | -ms-flex-pack: center;
16 | -webkit-box-align: center;
17 | align-items: center;
18 | -webkit-box-pack: center;
19 | justify-content: center;
20 | padding-top: 40px;
21 | padding-bottom: 40px;
22 | background-color: #f5f5f5;
23 | }
24 |
25 | .form-signin {
26 | background-color: #314b7b;
27 | width: 100%;
28 | max-width: 330px;
29 | padding: 15px;
30 | margin: 0 auto;
31 | }
32 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=source
11 | set BUILDDIR=build
12 | set SPHINXPROJ=pyTD
13 |
14 | if "%1" == "" goto help
15 |
16 | %SPHINXBUILD% >NUL 2>NUL
17 | if errorlevel 9009 (
18 | echo.
19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
20 | echo.installed, then set the SPHINXBUILD environment variable to point
21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
22 | echo.may add the Sphinx directory to PATH.
23 | echo.
24 | echo.If you don't have Sphinx installed, grab it from
25 | echo.http://sphinx-doc.org/
26 | exit /b 1
27 | )
28 |
29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
30 | goto end
31 |
32 | :help
33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
34 |
35 | :end
36 | popd
37 |
--------------------------------------------------------------------------------
/docs/source/index.rst:
--------------------------------------------------------------------------------
1 | pyTD: Python SDK for TD Ameritrade
2 | ==================================
3 |
4 | This documentation is organized as follows:
5 |
6 | - :ref:`getting_started`
7 | - :ref:`endpoints`
8 | - :ref:`tutorials`
9 | - :ref:`package_info`
10 |
11 | .. _getting_started:
12 | .. toctree::
13 | :maxdepth: 2
14 | :caption: Getting Started
15 |
16 | quickstart.rst
17 | install.rst
18 | configuration.rst
19 | auth.rst
20 |
21 | .. _endpoints:
22 | .. toctree::
23 | :maxdepth: 2
24 | :caption: Endpoints
25 |
26 | market.rst
27 | instruments.rst
28 |
29 | .. _tutorials:
30 | .. toctree::
31 | :maxdepth: 2
32 | :caption: Tutorials
33 | :glob:
34 |
35 | tutorials/*
36 |
37 | .. _package_info:
38 | .. toctree::
39 | :maxdepth: 2
40 | :caption: Package Info
41 |
42 | exceptions.rst
43 | testing.rst
44 | faq.rst
45 |
46 |
47 | Indices and tables
48 | ==================
49 |
50 | * :ref:`genindex`
51 | * :ref:`modindex`
52 | * :ref:`search`
53 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright (c) 2018 Addison Lynch
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/pyTD/tests/unit/__init__.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
--------------------------------------------------------------------------------
/pyTD/tests/integration/__init__.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
--------------------------------------------------------------------------------
/pyTD/cache/__init__.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | # flake8: noqa
24 |
25 | from pyTD.cache.disk_cache import DiskCache
26 | from pyTD.cache.mem_cache import MemCache
27 |
--------------------------------------------------------------------------------
/pyTD/auth/tokens/__init__.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | # flake8:noqa
24 |
25 | from pyTD.auth.tokens.access_token import AccessToken
26 | from pyTD.auth.tokens.empty_token import EmptyToken
27 | from pyTD.auth.tokens.refresh_token import RefreshToken
28 |
--------------------------------------------------------------------------------
/pyTD/tests/fixtures/mock_responses.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import pytest
24 |
25 | from pyTD.utils.testing import MockResponse
26 |
27 |
28 | @pytest.fixture(scope='function')
29 | def mock_400():
30 | r = MockResponse('{"error":"Bad token."', 400)
31 | return r
32 |
--------------------------------------------------------------------------------
/scripts/get_refresh_token.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python
2 |
3 | """
4 | Utility script to return cached refresh token if one exists given current
5 | environment configuration
6 |
7 | Attempts to find configuration file in configuration directory (located in
8 | either the value of TD_CONFIG_DIR or the default ~/.tdm). Raises an exception
9 | if a refresh token cannot be found or is not valid (expired or malformed)
10 | """
11 | import datetime
12 | import json
13 | import os
14 | import time
15 |
16 | CONFIG_DIR = os.getenv("TD_CONFIG_DIR") or os.path.expanduser("~/.tdm")
17 |
18 | CONSUMER_KEY = os.getenv("TD_CONSUMER_KEY")
19 | # Must have consumer key to locate DiskCache file
20 | if CONSUMER_KEY is None:
21 | raise ValueError("Environment variable TD_CONSUMER_KEY must be set")
22 |
23 | CONFIG_PATH = os.path.join(CONFIG_DIR, CONSUMER_KEY)
24 |
25 | try:
26 | with open(CONFIG_PATH, 'r') as f:
27 | json_data = json.load(f)
28 | refresh_token = json_data["refresh_token"]
29 | token = refresh_token["token"]
30 | now = datetime.datetime.now()
31 | now = int(time.mktime(now.timetuple()))
32 | access = int(refresh_token["access_time"])
33 | expires = int(refresh_token["expires_in"])
34 | expiry = access + expires
35 | if expiry > now:
36 | print(token)
37 | else:
38 | raise Exception("Refresh token expired")
39 | except Exception:
40 | raise Exception("Refresh token could not be retrieved")
41 |
--------------------------------------------------------------------------------
/pyTD/auth/tokens/access_token.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | from pyTD.auth.tokens.base import Token
24 |
25 |
26 | class AccessToken(Token):
27 | """
28 | Access Token object
29 | """
30 | def __repr__(self):
31 | fmt = ("AccessToken(token= %s, access_time = %s, expires_in = %s)")
32 | return fmt % (self.token, self.access_time, self.expires_in)
33 |
--------------------------------------------------------------------------------
/pyTD/auth/tokens/refresh_token.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | from pyTD.auth.tokens.base import Token
24 |
25 |
26 | class RefreshToken(Token):
27 | """
28 | Refresh Token object
29 | """
30 | def __repr__(self):
31 | fmt = ("RefreshToken(token= %s, access_time = %s, expires_in = %s)")
32 | return fmt % (self.token, self.access_time, self.expires_in)
33 |
--------------------------------------------------------------------------------
/pyTD/auth/tokens/empty_token.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 |
24 | class EmptyToken(object):
25 | """
26 | Empty token object. Returns not valid.
27 | """
28 | def __dict__(self):
29 | return {}
30 |
31 | def __repr__(self):
32 | return str(self)
33 |
34 | def __str__(self):
35 | return ("EmptyToken(valid: False)")
36 |
37 | @property
38 | def token(self):
39 | return None
40 |
41 | @property
42 | def valid(self):
43 | return False
44 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 |
3 | language: python
4 |
5 | python:
6 | - "2.7"
7 | - "3.4"
8 | - "3.5"
9 | - "3.6"
10 |
11 | env:
12 | global:
13 | # Doctr deploy key for addisonlynch/pyTD
14 | - secure: "I6xa7gmoDuKAdForRLD8bax6D1IG9wfWzUUZGUH+zrio7888tx2DMzbEowML3rfL2Kg40F6lkmuhmH/ak4bgOLIL5GbdTIDAQEo9hNTV4WdeDnAX5InQkzzpPuAMXxtfZKvDN3UVCKv5lCFoTtqjpB0OgOPhXMExmEljlPEqtlem1jmtzZYyetW9ZRYZ/AhfzvlVseqs7qUF0r7yJmE+Bh2Rtx8lgYcGXSnwT72PqQvaMbKHpRA5Cb7wp35Yxyq1hRJsMoCGfMWFjNAMyHwKuh/iHJ3g8B/A78aKnMHiZwjuKrjbUOxAdGivkAGtlqhVQ9jLNT1RO4AkLBFz6RkKALsMc01iPq+S2crZ85GIoJp+piF8C+IGEaI5AcpedvST0sAqSYCgTe/uWWO4oL1S645yJblZHR7bSg1FQAxWkYnB4ndnKnFKhQSPvx/6JH2IXcz7+LT3xKRanJqQ/OmZlb1Rm1Iqfzfsa1+hYdXAacxUGZM3P2PZXUUanAnKPjo3ckwd3Kv+gDFmFvlIqyI45jzLesGyAgyBlCsOMQ+th5PXus5cgp4Rn7wfZ1r6krvx99cM0Ihh89qOsJcHN/u1OD/dC2JyMFJHzou1wHhG9vKCMTHJcmWUAru5RDctPw1iIuUkQPIui+3t00wtyM24q1Jsw45J/UCZQtGsV1CmZsg="
15 |
16 | install:
17 | - pip install -qq flake8
18 | - pip install codecov
19 | - if [[ $TRAVIS_PYTHON_VERSION == 3.7 ]]; then
20 | pip install doctr sphinx sphinx_rtd_theme ipython matplotlib;
21 | pip install sphinxcontrib-napoleon;
22 | fi
23 | - python setup.py install
24 |
25 | script:
26 | - pytest --noweb pyTD/tests
27 | - pytest -k webtest
28 | - flake8 --version
29 | - flake8 pyTD
30 |
31 | after_success:
32 | - |
33 | if [[ $TRAVIS_PYTHON_VERSION == 3.7 ]]; then
34 | cd docs
35 | make html && make html
36 | cd ..
37 | doctr deploy devel --build-tags
38 | if [[ -z ${TRAVIS_TAG} ]]; then
39 | echo "Not a tagged build."
40 | else
41 | doctr deploy stable --build-tags
42 | fi
43 | fi
44 | - codecov --token='34052138-4c31-4bf0-96cf-fbc2f1e56e65'
45 |
--------------------------------------------------------------------------------
/docs/source/install.rst:
--------------------------------------------------------------------------------
1 | .. _install:
2 |
3 |
4 | Installing pyTD
5 | ===============
6 |
7 | pyTD supports Python 2.7, 3.4, 3.5, 3.6, and 3.7. The recommended
8 | installation method is via ``pip``:
9 |
10 | .. code-block:: bash
11 |
12 | $ pip install pyTD
13 |
14 |
15 | Below are the options for installing pyTD.
16 |
17 | .. warning:: After pyTD is installed, it must be configured. See the pyTD :ref:`Quick Start Guide` for more information.
18 |
19 | .. _install.dependencies:
20 |
21 | Dependencies
22 | ------------
23 |
24 | - requests
25 | - pandas
26 |
27 | For testing requirements, see `testing `__.
28 |
29 | Installation
30 | ------------
31 |
32 | The recommended installation method is ``pip``. For more information about
33 | installing Python and pip, see "The Hitchhiker's Guide to Python" `Installation
34 | Guides `__.
35 |
36 | Stable Release
37 | ~~~~~~~~~~~~~~
38 |
39 | .. code:: bash
40 |
41 | $ pip install pyTD
42 |
43 |
44 | Development Version
45 | ~~~~~~~~~~~~~~~~~~~
46 |
47 |
48 | .. code:: bash
49 |
50 | $ pip install git+https://github.com/addisonlynch/pyTD.git
51 |
52 | or
53 |
54 | .. code:: bash
55 |
56 | $ git clone https://github.com/addisonlynch/pyTD.git
57 | $ cd pyTD
58 | $ pip install .
59 |
60 | Older Versions
61 | ~~~~~~~~~~~~~~
62 |
63 | .. code:: bash
64 |
65 | $ pip install pyTD=0.0.1
66 |
67 | virtualenv
68 | ----------
69 |
70 | The use of
71 | `virtualenv `__
72 | is **highly** recommended as below:
73 |
74 | .. code:: bash
75 |
76 | $ pip install virtualenv
77 | $ virtualenv env
78 | $ source env/bin/activate
79 |
--------------------------------------------------------------------------------
/pyTD/tests/unit/test_exceptions.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | from pyTD.utils.exceptions import (AuthorizationError,
24 | ConfigurationError,
25 | SSLError)
26 |
27 |
28 | class TestExceptions(object):
29 |
30 | def test_auth_error(self):
31 | error = AuthorizationError("Authorization failed.")
32 | assert str(error) == "Authorization failed."
33 |
34 | def test_config_error(self):
35 | error = ConfigurationError("Configuration failed.")
36 | assert str(error) == "Configuration failed."
37 |
38 | def test_ssl_error(self):
39 | error = SSLError("SSL failed.")
40 | assert str(error) == "SSL failed."
41 |
--------------------------------------------------------------------------------
/docs/source/testing.rst:
--------------------------------------------------------------------------------
1 | .. _testing:
2 |
3 |
4 | Testing
5 | =======
6 |
7 |
8 | .. _testing.environment:
9 |
10 | Setting Up a Testing Environment
11 | --------------------------------
12 |
13 | 1. Install the testing :ref:`dependencies`
14 |
15 | .. code:: bash
16 |
17 | $ pip3 install -r requirements-dev.txt
18 |
19 | 2. Run the tests
20 |
21 | In the pyTD root directory, there is a shell script ``test.sh``. This script
22 | first verifies flake8 compliance, then runs all tests with pytest.
23 |
24 | .. _testing.writing-tests:
25 |
26 | Writing Tests
27 | -------------
28 |
29 | Marking
30 | ~~~~~~~
31 |
32 | All tests which require HTTP requests which are not mocked should be
33 | marked with ``pytest.mark.webtest``. These tests will be automatically skipped
34 | if pyTD has not been properly configured or does not have a valid access token
35 | (this is verified through ``pyTD.api.default_auth_ok``).
36 |
37 |
38 | Fixtures
39 | ~~~~~~~~
40 |
41 | A number of fixtures are used to provide instantiated objects (``api``,
42 | tokens, etc.) as well as parametrize tests. These fixtures can be found in the
43 | ``fixtures`` directory.
44 |
45 | .. seealso:: `pytest Fixtures Documentation
46 | `__
47 |
48 |
49 | .. _testing.dependencies:
50 |
51 | Testing Dependencies
52 | --------------------
53 |
54 | Tests
55 | ~~~~~
56 |
57 | - `pytest `__
58 | - `tox `__
59 | - `flake8 `__
60 |
61 |
62 | Documentation
63 | ~~~~~~~~~~~~~
64 |
65 | - `sphinx `__
66 | - `sphinx-autobuild `__
67 | - `sphinx-rtd-theme `__
68 | - `ipython `__
69 | - `matplotlib `__
70 |
--------------------------------------------------------------------------------
/pyTD/tests/integration/test_integrate.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import pytest
24 |
25 | from pyTD.api import api
26 | from pyTD.market import get_quotes
27 | from pyTD.utils.exceptions import AuthorizationError
28 | from pyTD.utils.testing import MockResponse
29 |
30 |
31 | class TestInvalidToken(object):
32 |
33 | def test_invalid_access_quote(self, valid_cache, sample_oid, sample_uri,
34 | monkeypatch):
35 | a = api(consumer_key=sample_oid, callback_url=sample_uri,
36 | cache=valid_cache)
37 |
38 | r = MockResponse('{"error":"Not Authrorized"}', 401)
39 |
40 | def _mock_handler(self, *args, **kwargs):
41 | return a.handle_response(r)
42 |
43 | a.request = _mock_handler
44 |
45 | with pytest.raises(AuthorizationError):
46 | get_quotes("AAPL", api=a)
47 |
--------------------------------------------------------------------------------
/pyTD/tests/test_helper.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import logging
24 | import os
25 | import pyTD
26 |
27 |
28 | logging.basicConfig(level=logging.INFO)
29 |
30 | consumer_key = os.getenv("TD_CONSUMER_KEY")
31 | refresh_token = os.getenv("TD_REFRESH_TOKEN")
32 |
33 | if refresh_token is None:
34 | raise EnvironmentError("Must set TD_REFRESH_TOKEN environment variable "
35 | "in order to run tests")
36 |
37 | init_data = {
38 | "token": refresh_token,
39 | "access_time": 10000000,
40 | "expires_in": 99999999999999
41 | }
42 |
43 | refresh_token = pyTD.auth.tokens.RefreshToken(options=init_data)
44 |
45 | cache = pyTD.cache.MemCache()
46 | cache.refresh_token = refresh_token
47 |
48 | pyTD.configure(consumer_key=consumer_key,
49 | callback_url="https://127.0.0.1:65010/td-callback",
50 | cache=cache)
51 |
--------------------------------------------------------------------------------
/pyTD/cache/mem_cache.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | from pyTD.cache.base import TokenCache
24 |
25 |
26 | class MemCache(TokenCache):
27 | """
28 | In-memory token cache for access and refresh tokens
29 |
30 | Usage
31 | -----
32 |
33 | >>> c = MemCache()
34 | >>> c.refresh_token = token
35 | >>> c.access_token = token
36 | """
37 | def clear(self):
38 | return self._create()
39 |
40 | def _create(self):
41 | self._refresh_token = None
42 | self._access_token = None
43 |
44 | def _exists(self):
45 | return True
46 |
47 | def _get(self, api_property_name):
48 | return self.__getattribute__("_%s" % api_property_name)
49 |
50 | def _set(self, api_property_name, value):
51 | return self.__setattr__("_%s" % api_property_name, value)
52 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # TODO file
2 | TODO
3 |
4 | # tox bin
5 | bin/
6 |
7 | # macOS attributes file
8 | ._.DS_Store
9 | .DS_Store
10 |
11 | # Sublime SFTP config
12 | sftp-config.json
13 |
14 | # Byte-compiled / optimized / DLL files
15 | __pycache__/
16 | *.py[cod]
17 | *$py.class
18 |
19 | # C extensions
20 | *.so
21 |
22 | # Distribution / packaging
23 | .Python
24 | build/
25 | develop-eggs/
26 | dist/
27 | downloads/
28 | eggs/
29 | .eggs/
30 | lib/
31 | lib64/
32 | parts/
33 | sdist/
34 | var/
35 | wheels/
36 | *.egg-info/
37 | .installed.cfg
38 | *.egg
39 | MANIFEST
40 |
41 | # PyInstaller
42 | # Usually these files are written by a python script from a template
43 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
44 | *.manifest
45 | *.spec
46 |
47 | # Installer logs
48 | pip-log.txt
49 | pip-delete-this-directory.txt
50 |
51 | # Unit test / coverage reports
52 | htmlcov/
53 | .tox/
54 | .coverage
55 | .coverage.*
56 | .cache
57 | nosetests.xml
58 | coverage.xml
59 | *.cover
60 | .hypothesis/
61 | .pytest_cache/
62 |
63 | # Translations
64 | *.mo
65 | *.pot
66 |
67 | # Django stuff:
68 | *.log
69 | local_settings.py
70 | db.sqlite3
71 |
72 | # Flask stuff:
73 | instance/
74 | .webassets-cache
75 |
76 | # Scrapy stuff:
77 | .scrapy
78 |
79 | # Sphinx documentation
80 | docs/_build/
81 |
82 | # PyBuilder
83 | target/
84 |
85 | # Jupyter Notebook
86 | .ipynb_checkpoints
87 |
88 | # IPython
89 | profile_default/
90 | ipython_config.py
91 |
92 | # pyenv
93 | .python-version
94 |
95 | # celery beat schedule file
96 | celerybeat-schedule
97 |
98 | # SageMath parsed files
99 | *.sage.py
100 |
101 | # Environments
102 | .env
103 | .venv
104 | env/
105 | venv/
106 | ENV/
107 | env.bak/
108 | venv.bak/
109 |
110 | # Spyder project settings
111 | .spyderproject
112 | .spyproject
113 |
114 | # Rope project settings
115 | .ropeproject
116 |
117 | # mkdocs documentation
118 | /site
119 |
120 | # mypy
121 | .mypy_cache/
122 | .dmypy.json
123 | dmypy.json
124 |
--------------------------------------------------------------------------------
/pyTD/tests/unit/test_resource.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import pytest
24 |
25 |
26 | from pyTD.api import api
27 | from pyTD.resource import Get
28 | from pyTD.utils.exceptions import TDQueryError
29 | from pyTD.utils.testing import MockResponse
30 |
31 |
32 | @pytest.fixture(params=[
33 | MockResponse("", 200),
34 | MockResponse('{"error":"Not Found."}', 200)], ids=[
35 | "Empty string",
36 | '"Error" in response',
37 | ])
38 | def bad_json(request):
39 | return request.param
40 |
41 |
42 | class TestResource(object):
43 |
44 | def test_get_raises_json(self, bad_json, valid_cache,
45 | sample_oid, sample_uri):
46 | a = api(consumer_key=sample_oid, callback_url=sample_uri,
47 | cache=valid_cache)
48 | a.request = lambda s, *a, **k: bad_json
49 | resource = Get(api=a)
50 |
51 | with pytest.raises(TDQueryError):
52 | resource.get()
53 |
--------------------------------------------------------------------------------
/pyTD/tests/conftest.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | # flake8: noqa
24 | import pytest
25 |
26 | # fixture routing
27 | from pyTD.tests.fixtures import sample_oid
28 | from pyTD.tests.fixtures import sample_uri
29 | from pyTD.tests.fixtures import valid_refresh_token, valid_access_token
30 | from pyTD.tests.fixtures import set_env, del_env
31 | from pyTD.tests.fixtures import valid_cache, invalid_cache
32 |
33 | # mock responses routing
34 | from pyTD.tests.fixtures.mock_responses import mock_400
35 |
36 |
37 | def pytest_addoption(parser):
38 | parser.addoption(
39 | "--noweb", action="store_true", default=False, help="Ignore web tests"
40 | )
41 |
42 | def pytest_collection_modifyitems(config, items):
43 | if config.getoption("--noweb"):
44 | skip_web = pytest.mark.skip(reason="--noweb option passed. Skipping "
45 | "webtest.")
46 | for item in items:
47 | if "webtest" in item.keywords:
48 | item.add_marker(skip_web)
49 |
--------------------------------------------------------------------------------
/pyTD/tests/unit/test_cache.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import pytest
24 |
25 | from pyTD.cache import MemCache
26 | from pyTD.auth.tokens import EmptyToken
27 |
28 |
29 | @pytest.fixture(scope='function', autouse=True)
30 | def full_consumer_key():
31 | return "TEST@AMER.OAUTHAP"
32 |
33 |
34 | class TestMemCache(object):
35 |
36 | def test_default_values(self):
37 | c = MemCache()
38 |
39 | assert isinstance(c.refresh_token, EmptyToken)
40 | assert isinstance(c.access_token, EmptyToken)
41 |
42 | def test_set_token(self, valid_refresh_token):
43 | c = MemCache()
44 | c.refresh_token = valid_refresh_token
45 |
46 | assert c.refresh_token.token == "validtoken"
47 | assert c.refresh_token.expires_in == 1000000
48 |
49 | def test_clear(self, valid_refresh_token, valid_access_token):
50 | c = MemCache()
51 | c.refresh_token = valid_refresh_token
52 | c.access_token == valid_access_token
53 |
54 | c.clear()
55 | assert isinstance(c.refresh_token, EmptyToken)
56 | assert isinstance(c.access_token, EmptyToken)
57 |
--------------------------------------------------------------------------------
/pyTD/instruments/__init__.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | from pyTD.instruments.base import Instruments
24 |
25 |
26 | def get_instrument(*args, **kwargs):
27 | """
28 | Retrieve instrument from CUSIP ID from the Get Instrument endpoint
29 |
30 | Parameters
31 | ----------
32 | symbol: str
33 | A CUSIP ID or symbol
34 | output_format: str, default "pandas", optional
35 | Desired output format. "pandas" or "json"
36 | """
37 | return Instruments(*args, **kwargs).execute()
38 |
39 |
40 | def get_instruments(*args, **kwargs):
41 | """
42 | Search or retrieve instrument data, including fundamental data
43 |
44 | Parameters
45 | ----------
46 | symbol: str
47 | A CUSIP ID, symbol, regular expression, or snippet (depends on the
48 | value of the "projection" variable)
49 | projection: str, default symbol-search, optional
50 | Type of request (see documentation)
51 | output_format: str, default "pandas", optional
52 | Desired output format. "pandas" or "json"
53 | """
54 | return Instruments(*args, **kwargs).execute()
55 |
--------------------------------------------------------------------------------
/pyTD/cache/base.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | from pyTD.auth.tokens import EmptyToken
24 |
25 |
26 | def surface_property(api_property_name, docstring=None):
27 | def getter(self):
28 | return self._get(api_property_name) or EmptyToken()
29 |
30 | def setter(self, value):
31 | if isinstance(value, EmptyToken):
32 | self._set(api_property_name, None)
33 | else:
34 | self._set(api_property_name, value)
35 |
36 | return property(getter, setter, doc=docstring)
37 |
38 |
39 | class TokenCache(object):
40 | """
41 | Base class for auth token caches
42 | """
43 | refresh_token = surface_property("refresh_token")
44 | access_token = surface_property("access_token")
45 |
46 | def __init__(self):
47 | self._create()
48 |
49 | def clear(self):
50 | raise NotImplementedError
51 |
52 | def _create(self):
53 | raise NotImplementedError
54 |
55 | def _exists(self):
56 | raise NotImplementedError
57 |
58 | def _get(self):
59 | raise NotImplementedError
60 |
61 | def _set(self):
62 | raise NotImplementedError
63 |
--------------------------------------------------------------------------------
/pyTD/tests/integration/test_api_integrate.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | from pyTD.instruments import get_instrument
24 | from pyTD.market import get_quotes
25 | from pyTD.api import api
26 |
27 | from pyTD.utils.testing import MockResponse
28 |
29 |
30 | class TestMarketAPI(object):
31 |
32 | def test_quote_arg_api(self, valid_cache, sample_oid, sample_uri):
33 | a = api(consumer_key=sample_oid, callback_url=sample_uri,
34 | cache=valid_cache)
35 | r = MockResponse('{"symbol":"AAPL","quote":155.34}', 200)
36 |
37 | a.request = lambda s, *a, **k: r
38 |
39 | q = get_quotes("AAPL", api=a, output_format='json')
40 |
41 | assert isinstance(q, dict)
42 | assert q["symbol"] == "AAPL"
43 |
44 | def test_instrument_arg_api(self, valid_cache, sample_oid, sample_uri):
45 | a = api(consumer_key=sample_oid, callback_url=sample_uri,
46 | cache=valid_cache)
47 |
48 | r = MockResponse('{"symbol":"ORCL"}', 200)
49 |
50 | a.request = lambda s, *a, **k: r
51 |
52 | i = get_instrument("68389X105", api=a, output_format='json')
53 |
54 | assert isinstance(i, dict)
55 | assert i["symbol"] == "ORCL"
56 |
--------------------------------------------------------------------------------
/pyTD/compat/__init__.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import sys
24 | from distutils.version import LooseVersion
25 |
26 | import pandas as pd
27 |
28 | PY3 = sys.version_info >= (3, 0)
29 |
30 | PANDAS_VERSION = LooseVersion(pd.__version__)
31 |
32 | PANDAS_0190 = (PANDAS_VERSION >= LooseVersion('0.19.0'))
33 | PANDAS_0230 = (PANDAS_VERSION >= LooseVersion('0.23.0'))
34 |
35 | if PANDAS_0190:
36 | from pandas.api.types import is_number
37 | else:
38 | from pandas.core.common import is_number # noqa
39 |
40 | if PANDAS_0230:
41 | from pandas.core.dtypes.common import is_list_like
42 | else:
43 | from pandas.core.common import is_list_like # noqa
44 |
45 | if PY3:
46 | from urllib.error import HTTPError
47 | from urllib.parse import urlparse, urlencode, parse_qs
48 | from io import StringIO
49 | from http.server import HTTPServer, BaseHTTPRequestHandler
50 | from mock import MagicMock
51 | else:
52 | from urllib2 import HTTPError # noqa
53 | from urlparse import urlparse, parse_qs # noqa
54 | from urllib import urlencode # noqa
55 | from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler # noqa
56 | import StringIO # noqa
57 | from mock import MagicMock # noqa
58 |
--------------------------------------------------------------------------------
/pyTD/__init__.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import os
24 | import logging
25 |
26 | __author__ = 'Addison Lynch'
27 | __version__ = '0.0.1'
28 |
29 | DEFAULT_CONFIG_DIR = '~/.tdm'
30 | CONFIG_DIR = os.path.expanduser(os.getenv("TD_CONFIG_DIR", DEFAULT_CONFIG_DIR))
31 |
32 | DEFAULT_SSL_DIR = os.path.join(CONFIG_DIR, 'ssl')
33 | PACKAGE_DIR = os.path.abspath(__name__)
34 |
35 | BASE_URL = 'https://api.tdameritrade.com/v1/'
36 | BASE_AUTH_URL = 'https://auth.tdameritrade.com/auth'
37 |
38 | # routing for configure
39 | from pyTD.api import configure # noqa
40 |
41 | # Store logging level
42 | log_level = "INFO"
43 | LEVEL = getattr(logging, os.getenv("TD_LOG_LEVEL", log_level))
44 |
45 | # Set logging level
46 | logger = logging.getLogger(__name__)
47 | logger.setLevel(LEVEL)
48 |
49 | # create formatter and add it to the handlers
50 | fh = logging.FileHandler(os.path.join(CONFIG_DIR, 'pyTD.log'), delay=True)
51 | file_format = logging.Formatter(
52 | '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
53 | fh.setFormatter(file_format)
54 | fh.setLevel(logging.DEBUG)
55 | logger.addHandler(fh)
56 |
57 | console_format = logging.Formatter(
58 | '%(levelname)s - %(message)s')
59 | ch = logging.StreamHandler()
60 | ch.setFormatter(console_format)
61 | logger.addHandler(ch)
62 |
--------------------------------------------------------------------------------
/pyTD/tests/integration/test_api_cache.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | from pyTD.api import api
24 | from pyTD.auth.tokens import EmptyToken
25 | from pyTD.cache import MemCache
26 |
27 |
28 | class TestAPICache(object):
29 |
30 | def test_api_init_cache(self, set_env, sample_oid, sample_uri):
31 | a = api(consumer_key=sample_oid, callback_url=sample_uri,
32 | store_tokens=False)
33 |
34 | assert isinstance(a.cache, MemCache)
35 | assert isinstance(a.cache.refresh_token, EmptyToken)
36 | assert isinstance(a.cache.access_token, EmptyToken)
37 |
38 | def test_api_pass_cache(self, set_env, sample_oid, sample_uri,
39 | valid_access_token, valid_refresh_token):
40 | c = MemCache()
41 |
42 | c.refresh_token = valid_refresh_token
43 | c.access_token = valid_access_token
44 |
45 | assert valid_access_token.valid is True
46 |
47 | a = api(consumer_key=sample_oid, callback_url=sample_uri,
48 | store_tokens=False, cache=c)
49 |
50 | assert isinstance(a.cache, MemCache)
51 | assert a.cache.refresh_token == c.refresh_token
52 | assert a.cache.access_token == c.access_token
53 |
54 | assert a.cache.access_token.valid is True
55 |
--------------------------------------------------------------------------------
/docs/source/instruments.rst:
--------------------------------------------------------------------------------
1 | .. _instruments:
2 |
3 | Instruments
4 | ===========
5 |
6 | The `Instruments `__
7 | endpoints allow for retrieval of instrument and fundamental
8 | data. These endpoints are `Search Instruments
9 | `__ and
10 | `Get Instrument
11 | `__.
12 |
13 | .. _instruments.get-instrument:
14 |
15 | Get Instrument
16 | --------------
17 |
18 | pyTD provides access to Get Instruments through the ``get_instrument``
19 | function. Simply enter a symbol:
20 |
21 | .. ipython:: python
22 |
23 | from pyTD.instruments import get_instrument
24 |
25 | get_instrument("AAPL")
26 |
27 | or a CUSIP ID:
28 |
29 | .. ipython:: python
30 |
31 | get_instrument("68389X105")
32 |
33 | .. _instruments.search-instruments:
34 |
35 | Search Instruments
36 | ------------------
37 |
38 | Search Instruments is implemented through ``get_instruments``.
39 |
40 | ``projection``
41 | ~~~~~~~~~~~~~~
42 |
43 | There are five types of searches which can be performed:
44 |
45 | 1. ``symbol-search`` (default): Retrieve instrument data of a specific symbol
46 | or CUSIP
47 | (similar to ``get_instrument``)
48 |
49 | .. ipython:: python
50 |
51 | from pyTD.instruments import get_instruments
52 |
53 | get_instruments("AAPL")
54 |
55 |
56 | 2. ``symbol-regex``: Retrieve instrument data for all symbols matching regex.
57 | Example: ``symbol=XYZ.*`` will return all symbols beginning with XYZ
58 |
59 | .. ipython:: python
60 |
61 | get_instruments("AAP.*", projection="symbol-regex")
62 |
63 |
64 | 3. ``desc-search``: Retrieve instrument data for instruments whose description
65 | contains the word supplied. Example: ``symbol=FakeCompany`` will return all
66 | instruments with FakeCompany in the description.
67 |
68 | .. ipython:: python
69 |
70 | get_instruments("computer", projection="desc-search")
71 |
72 |
73 | 4. ``desc-regex``: Search description with full regex support. Example:
74 | ``symbol=XYZ.[A-C]`` returns all instruments whose descriptions contain a word
75 | beginning with XYZ followed by a character A through C.
76 |
77 | .. ipython:: python
78 |
79 | get_instruments("COM.*", projection="desc-regex")
80 |
81 |
82 | 5. ``fundamental``: Returns fundamental data for a single instrument specified by exact symbol.
83 |
84 | .. ipython:: python
85 |
86 | get_instruments("AAPL", projection="fundamental").head()
87 |
--------------------------------------------------------------------------------
/pyTD/auth/__init__.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | # flake8:noqa
24 |
25 | import logging
26 |
27 | from functools import wraps
28 |
29 | from pyTD.auth.manager import TDAuthManager
30 | from pyTD.auth.server import TDAuthServer
31 | from pyTD.utils import yn_require
32 | from pyTD.utils.exceptions import AuthorizationError
33 |
34 |
35 | logger = logging.getLogger(__name__)
36 |
37 |
38 | def auth_check(func):
39 | @wraps(func)
40 | def _authenticate_wrapper(self, *args, **kwargs):
41 | if self.api.auth_valid is True:
42 | return func(self, *args, **kwargs)
43 | else:
44 | if self.api.refresh_valid is False:
45 | logger.warning("Need new refresh token.")
46 | choice = yn_require("Would you like to authorize a new "
47 | "refresh token?")
48 | if choice is True:
49 | self.api.refresh_auth()
50 | else:
51 | raise AuthorizationError("Refresh token "
52 | "needed for access.")
53 | else:
54 | self.api.auth.refresh_access_token()
55 | if self.api.auth_valid is True:
56 | return func(self, *args, **kwargs)
57 | else:
58 | raise AuthorizationError("Authorization could not be "
59 | "completed.")
60 | return _authenticate_wrapper
61 |
--------------------------------------------------------------------------------
/pyTD/resource.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import logging
24 |
25 | from pyTD import BASE_URL
26 | from pyTD.api import default_api
27 | from pyTD.utils.exceptions import TDQueryError
28 |
29 | logger = logging.getLogger(__name__)
30 |
31 |
32 | class Resource(object):
33 | """
34 | Base class for all REST services
35 | """
36 | _BASE_URL = BASE_URL
37 |
38 | def __init__(self, api=None):
39 | self.api = api or default_api()
40 |
41 | @property
42 | def url(self):
43 | return self._BASE_URL
44 |
45 | @property
46 | def params(self):
47 | return {}
48 |
49 | @property
50 | def data(self):
51 | return {}
52 |
53 | @property
54 | def headers(self):
55 | return {
56 | "content-type": "application/json"
57 | }
58 |
59 |
60 | class Get(Resource):
61 | """
62 | GET requests
63 | """
64 | def get(self, url=None, params=None):
65 | params = params or self.params
66 | url = url or self.url
67 |
68 | response = self.api.request("GET", url=url, params=params)
69 |
70 | # Convert GET requests to JSON
71 | try:
72 | json_data = response.json()
73 | except ValueError:
74 | raise TDQueryError(message="An error occurred during the query.",
75 | response=response)
76 | if "error" in json_data:
77 | raise TDQueryError(response=response)
78 | return json_data
79 |
--------------------------------------------------------------------------------
/pyTD/instruments/base.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | from pyTD.auth import auth_check
24 | from pyTD.resource import Get
25 | from pyTD.utils.exceptions import ResourceNotFound
26 |
27 |
28 | class Instruments(Get):
29 | """
30 | Class for retrieving instruments
31 | """
32 | def __init__(self, symbol, **kwargs):
33 | self.symbol = symbol
34 | self.output_format = kwargs.get("output_format", "pandas")
35 | self.projection = kwargs.get("projection", "symbol-search")
36 | super(Instruments, self).__init__(kwargs.get("api", None))
37 |
38 | @property
39 | def url(self):
40 | return "%s/instruments" % self._BASE_URL
41 |
42 | @property
43 | def params(self):
44 | return {
45 | "symbol": self.symbol,
46 | "projection": self.projection
47 | }
48 |
49 | def _convert_output(self, out):
50 | import pandas as pd
51 | if self.projection == "fundamental":
52 | return pd.DataFrame({self.symbol:
53 | out[self.symbol]["fundamental"]})
54 | return pd.DataFrame(out)
55 |
56 | @auth_check
57 | def execute(self):
58 | data = self.get()
59 | if not data:
60 | raise ResourceNotFound("Instrument data for %s not"
61 | " found." % self.symbol)
62 | if self.output_format == "pandas":
63 | return self._convert_output(data)
64 | else:
65 | return data
66 |
--------------------------------------------------------------------------------
/pyTD/market/quotes.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | from pyTD.auth import auth_check
24 | from pyTD.market.base import MarketData
25 | from pyTD.utils import _handle_lists
26 | from pyTD.utils.exceptions import ResourceNotFound
27 |
28 |
29 | class Quotes(MarketData):
30 | """
31 | Class for retrieving data from the Get Quote and Get Quotes endpoints.
32 |
33 | Parameters
34 | ----------
35 | symbols : string, array-like object (list, tuple, Series), or DataFrame
36 | Desired symbols for retrieval
37 | output_format: str, optional, default 'pandas'
38 | Desired output format (json or Pandas DataFrame)
39 | api: pyTD.api.api object, optional
40 | A pyTD api object. If not passed, API requestor defaults to
41 | pyTD.api.default_api
42 | """
43 | def __init__(self, symbols, output_format='pandas', api=None):
44 | self.symbols = _handle_lists(symbols)
45 | if len(self.symbols) > 100:
46 | raise ValueError("Please input a valid symbol or list of up to "
47 | "100 symbols")
48 | super(Quotes, self).__init__(output_format, api)
49 |
50 | @property
51 | def resource(self):
52 | return "quotes"
53 |
54 | @property
55 | def params(self):
56 | return {
57 | "symbol": ','.join(self.symbols)
58 | }
59 |
60 | @auth_check
61 | def execute(self):
62 | data = self.get()
63 | if not data:
64 | raise ResourceNotFound(data, message="Quote for symbol %s not "
65 | "found." % self.symbols)
66 | else:
67 | return self._output_format(data)
68 |
--------------------------------------------------------------------------------
/pyTD/auth/tokens/base.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import datetime
24 |
25 | from pyTD.utils import to_timestamp
26 |
27 |
28 | class Token(object):
29 | """
30 | Token object base class.
31 |
32 | Parameters
33 | ----------
34 | token: str
35 | Token value
36 | access_time: int
37 | expires_in: int
38 |
39 | """
40 | def __init__(self, options=None, **kwargs):
41 |
42 | kwargs.update(options or {})
43 |
44 | self.token = kwargs['token']
45 | self.access_time = kwargs['access_time']
46 | self.expires_in = kwargs['expires_in']
47 |
48 | def __dict__(self):
49 | return {
50 | "token": self.token,
51 | "access_time": self.access_time,
52 | "expires_in": self.expires_in
53 | }
54 |
55 | def __eq__(self, other):
56 | if isinstance(other, Token):
57 | t = self.token == other.token
58 | a = self.access_time == other.access_time
59 | e = self.expires_in == other.expires_in
60 | return t and a and e
61 |
62 | def __ne__(self, other):
63 | if isinstance(other, Token):
64 | t = self.token == other.token
65 | a = self.access_time == other.access_time
66 | e = self.expires_in == other.expires_in
67 | return t and a and e
68 |
69 | def __repr__(self):
70 | fmt = ("Token(token= %s, access_time = %s, expires_in = %s)")
71 | return fmt % (self.token, self.access_time, self.expires_in)
72 |
73 | def __str__(self):
74 | return self.token
75 |
76 | @property
77 | def expiry(self):
78 | return self.access_time + self.expires_in
79 |
80 | @property
81 | def valid(self):
82 | now = to_timestamp(datetime.datetime.now())
83 | return True if self.expiry > now else False
84 |
--------------------------------------------------------------------------------
/pyTD/market/base.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import logging
24 |
25 | from pyTD.auth import auth_check
26 | from pyTD.resource import Get
27 |
28 | logger = logging.getLogger(__name__)
29 |
30 |
31 | class MarketData(Get):
32 | """
33 | Base class for retrieving market-based information. This includes the
34 | following endpoint groups:
35 | - Market Hours
36 | - Movers
37 | - Option Chains
38 | - Price History
39 | - Quotes
40 |
41 | Parameters
42 | ----------
43 | symbols: str or list-like, optional
44 | A symbol or list of symbols
45 | output_format: str, optional, default 'json'
46 | Desired output format (json or Pandas DataFrame)
47 | api: pyTD.api.api object, optional
48 | A pyTD api object. If not passed, API requestor defaults to
49 | pyTD.api.default_api
50 | """
51 | def __init__(self, output_format='pandas', api=None):
52 | self.output_format = output_format
53 | super(MarketData, self).__init__(api)
54 |
55 | @property
56 | def endpoint(self):
57 | return "marketdata"
58 |
59 | @property
60 | def resource(self):
61 | raise NotImplementedError
62 |
63 | @property
64 | def url(self):
65 | return "%s%s/%s" % (self._BASE_URL, self.endpoint, self.resource)
66 |
67 | def _convert_output(self, out):
68 | import pandas as pd
69 | return pd.DataFrame(out)
70 |
71 | @auth_check
72 | def execute(self):
73 | out = self.get()
74 | return self._output_format(out)
75 |
76 | def _output_format(self, out):
77 | if self.output_format == 'json':
78 | return out
79 | elif self.output_format == 'pandas':
80 | return self._convert_output(out)
81 | else:
82 | raise ValueError("Please enter a valid output format.")
83 |
--------------------------------------------------------------------------------
/pyTD/tests/fixtures/__init__.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import datetime
24 |
25 | import pytest
26 |
27 | from pyTD.auth.tokens import RefreshToken, AccessToken
28 | from pyTD.cache import MemCache
29 | from pyTD.utils import to_timestamp
30 |
31 |
32 | valid_params = {
33 | "token": "validtoken",
34 | "access_time": to_timestamp(datetime.datetime.now()) - 15000,
35 | "expires_in": 1000000,
36 | }
37 |
38 | invalid_params = {
39 | "token": "invalidtoken",
40 | "access_time": to_timestamp(datetime.datetime.now()) - 15000,
41 | "expires_in": 10000,
42 | }
43 |
44 |
45 | @pytest.fixture(scope='function')
46 | def valid_cache():
47 | r = RefreshToken(valid_params)
48 | a = AccessToken(valid_params)
49 | c = MemCache()
50 | c.refresh_token = r
51 | c.access_token = a
52 | return c
53 |
54 |
55 | @pytest.fixture(scope='function')
56 | def invalid_cache():
57 | r = RefreshToken(invalid_params)
58 | a = AccessToken(invalid_params)
59 | c = MemCache()
60 | c.refresh_token = r
61 | c.access_token = a
62 | return c
63 |
64 |
65 | @pytest.fixture(scope='session')
66 | def valid_refresh_token():
67 | return RefreshToken(valid_params)
68 |
69 |
70 | @pytest.fixture(scope='session')
71 | def valid_access_token():
72 | return AccessToken(valid_params)
73 |
74 |
75 | @pytest.fixture(scope='session', autouse=True)
76 | def sample_oid():
77 | return "TEST10@AMER.OAUTHAP"
78 |
79 |
80 | @pytest.fixture(scope='session', autouse=True)
81 | def sample_uri():
82 | return "https://127.0.0.1:60000/td-callback"
83 |
84 |
85 | @pytest.fixture(scope="function")
86 | def set_env(monkeypatch, sample_oid, sample_uri):
87 | monkeypatch.setenv("TD_CONSUMER_KEY", sample_oid)
88 | monkeypatch.setenv("TD_CALLBACK_URL", sample_uri)
89 |
90 |
91 | @pytest.fixture(scope="function")
92 | def del_env(monkeypatch):
93 | monkeypatch.delenv("TD_CONSUMER_KEY", raising=False)
94 | monkeypatch.delenv("TD_CALLBACK_URL", raising=False)
95 |
--------------------------------------------------------------------------------
/pyTD/market/movers.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | from pyTD.market.base import MarketData
24 | from pyTD.utils import _handle_lists
25 |
26 |
27 | class Movers(MarketData):
28 | """
29 | Class for retrieving data from the Get Market Hours endpoint.
30 |
31 | Parameters
32 | ----------
33 | markets : str
34 | Ticker of market for retrieval
35 | direction : str, default 'up', optional
36 | To return movers with the specified directions of up or down
37 | change: str, default 'percent', optional
38 | To return movers with the specified change types of percent or value
39 | output_format: str, optional, default 'json'
40 | Desired output format (json or Pandas DataFrame)
41 | api: pyTD.api.api object, optional
42 | A pyTD api object. If not passed, API requestor defaults to
43 | pyTD.api.default_api
44 |
45 | WARNING: this endpoint is often not functional outside of trading hours.
46 | """
47 |
48 | def __init__(self, symbols, direction='up', change='percent',
49 | output_format='pandas', api=None):
50 | self.direction = direction
51 | self.change = change
52 | err_msg = "Please input a valid market ticker (ex. $DJI)."
53 | self.symbols = _handle_lists(symbols, mult=False, err_msg=err_msg)
54 | super(Movers, self).__init__(output_format, api)
55 |
56 | def _convert_output(self, out):
57 | import pandas as pd
58 | return pd.DataFrame(out).set_index("symbol")
59 |
60 | @property
61 | def params(self):
62 | return {
63 | 'change': self.change,
64 | 'direction': self.direction
65 | }
66 |
67 | @property
68 | def resource(self):
69 | return "movers"
70 |
71 | @property
72 | def url(self):
73 | return "%s%s/%s/%s" % (self._BASE_URL, self.endpoint, self.symbols,
74 | self.resource)
75 |
--------------------------------------------------------------------------------
/pyTD/tests/unit/test_tokens.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import datetime
24 | import pytest
25 |
26 | from pyTD.auth.tokens import RefreshToken, AccessToken, EmptyToken
27 | from pyTD.utils import to_timestamp
28 |
29 |
30 | valid_params = {
31 | "token": "validtoken",
32 | "access_time": to_timestamp(datetime.datetime.now()) - 15000,
33 | "expires_in": 1000000,
34 | }
35 |
36 | invalid_params = {
37 | "token": "invalidtoken",
38 | "access_time": to_timestamp(datetime.datetime.now()) - 15000,
39 | "expires_in": 10000,
40 | }
41 |
42 |
43 | @pytest.fixture(params=[
44 | RefreshToken,
45 | AccessToken
46 | ], ids=["Refresh", "Access"], scope='function')
47 | def tokens(request):
48 | return request.param
49 |
50 |
51 | @pytest.fixture(params=[
52 | (valid_params, True),
53 | (invalid_params, False)
54 | ], ids=["valid", "invalid"])
55 | def validity(request):
56 | return request.param
57 |
58 |
59 | class TestTokens(object):
60 |
61 | def test_empty_token(self):
62 | t = EmptyToken()
63 |
64 | assert t.valid is False
65 |
66 | def test_new_token_dict_validity(self, tokens, validity):
67 | t = tokens(validity[0])
68 |
69 | assert t.valid is validity[1]
70 |
71 | assert t.token == validity[0]["token"]
72 | assert t.access_time == validity[0]["access_time"]
73 | assert t.expires_in == validity[0]["expires_in"]
74 |
75 | def test_new_token_params_validity(self, tokens, validity):
76 | t = tokens(**validity[0])
77 |
78 | assert t.valid is validity[1]
79 |
80 | assert t.token == validity[0]["token"]
81 | assert t.access_time == validity[0]["access_time"]
82 | assert t.expires_in == validity[0]["expires_in"]
83 |
84 | def test_token_equality(self, valid_refresh_token):
85 | token1 = RefreshToken(token="token", access_time=1, expires_in=1)
86 | token2 = RefreshToken({"token": "token", "access_time": 1,
87 | "expires_in": 1})
88 | assert token1 == token2
89 |
--------------------------------------------------------------------------------
/pyTD/tests/unit/test_utils.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import datetime
24 | import pytest
25 | import pandas as pd
26 |
27 | from pyTD.utils import _handle_lists, _sanitize_dates
28 |
29 |
30 | @pytest.fixture(params=[
31 | 1,
32 | "test"
33 | ], ids=[
34 | "int",
35 | "string"
36 | ])
37 | def single(request):
38 | return request.param
39 |
40 |
41 | @pytest.fixture(params=[
42 | [1, 2, 3],
43 | (1, 2, 3),
44 | pd.DataFrame([], index=[1, 2, 3]),
45 | pd.Series([1, 2, 3]),
46 | ], ids=[
47 | "list",
48 | "tuple",
49 | "DataFrame",
50 | "Series"
51 | ])
52 | def mult(request):
53 | return request.param
54 |
55 |
56 | class TestUtils(object):
57 |
58 | def test_handle_lists_sing(self, single):
59 | assert _handle_lists(single, mult=False) == single
60 | assert _handle_lists(single) == [single]
61 |
62 | def test_handle_lists_mult(self, mult):
63 | assert _handle_lists(mult) == [1, 2, 3]
64 |
65 | def test_handle_lists_err(self, mult):
66 | with pytest.raises(ValueError):
67 | _handle_lists(mult, mult=False)
68 |
69 | def test_sanitize_dates_years(self):
70 | expected = (datetime.datetime(2017, 1, 1),
71 | datetime.datetime(2018, 1, 1))
72 | assert _sanitize_dates(2017, 2018) == expected
73 |
74 | def test_sanitize_dates_default(self):
75 | exp_start = datetime.datetime(2017, 1, 1, 0, 0)
76 | exp_end = datetime.datetime.today()
77 | start, end = _sanitize_dates(None, None)
78 |
79 | assert start == exp_start
80 | assert end.date() == exp_end.date()
81 |
82 | def test_sanitize_dates(self):
83 | start = datetime.datetime(2017, 3, 4)
84 | end = datetime.datetime(2018, 3, 9)
85 |
86 | assert _sanitize_dates(start, end) == (start, end)
87 |
88 | def test_sanitize_dates_error(self):
89 | start = datetime.datetime(2018, 1, 1)
90 | end = datetime.datetime(2017, 1, 1)
91 |
92 | with pytest.raises(ValueError):
93 | _sanitize_dates(start, end)
94 |
--------------------------------------------------------------------------------
/pyTD/utils/exceptions.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import logging
24 |
25 |
26 | logger = logging.getLogger(__name__)
27 |
28 |
29 | class AuthorizationError(Exception):
30 | """
31 | This error is thrown when an error with authorization occurs
32 | """
33 |
34 | def __init__(self, msg):
35 | self.msg = msg
36 |
37 | def __str__(self):
38 | return self.msg
39 |
40 |
41 | class SSLError(AuthorizationError):
42 | pass
43 |
44 |
45 | class TDQueryError(Exception):
46 | def __init__(self, response=None, content=None, message=None):
47 | self.response = response
48 | self.content = content
49 | self.message = message
50 |
51 | def __str__(self):
52 | message = self.message
53 | if hasattr(self.response, 'status_code'):
54 | message += " Response status: %s." % (self.response.status_code)
55 | if hasattr(self.response, 'reason'):
56 | message += " Response message: %s." % (self.response.reason)
57 | if hasattr(self.response, 'text'):
58 | message += " Response text: %s." % (self.response.text)
59 | if hasattr(self.response, 'url'):
60 | message += " Request URL: %s." % (self.response.url)
61 | if self.content is not None:
62 | message += " Error message: %s" % str(self.content)
63 | return message
64 |
65 |
66 | class ClientError(TDQueryError):
67 |
68 | _DEFAULT = "There was a client error with your request."
69 |
70 | def __init__(self, response, content=None, message=None):
71 | pass
72 |
73 |
74 | class ServerError(TDQueryError):
75 | pass
76 |
77 |
78 | class ResourceNotFound(TDQueryError):
79 | pass
80 |
81 |
82 | class ValidationError(TDQueryError):
83 | pass
84 |
85 |
86 | class ForbiddenAccess(TDQueryError):
87 | pass
88 |
89 |
90 | class ConnectionError(TDQueryError):
91 | pass
92 |
93 |
94 | class Redirection(TDQueryError):
95 | pass
96 |
97 |
98 | class ConfigurationError(Exception):
99 | def __init__(self, message):
100 | self.message = message
101 |
102 | def __str__(self):
103 | return self.message
104 |
105 |
106 | class CacheError(Exception):
107 | pass
108 |
--------------------------------------------------------------------------------
/pyTD/market/hours.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import datetime
24 | import pandas as pd
25 |
26 | from pyTD.market.base import MarketData
27 | from pyTD.utils import _handle_lists
28 |
29 |
30 | class MarketHours(MarketData):
31 | """
32 | Class for retrieving data from the Get Market Hours endpoint.
33 |
34 | Parameters
35 | ----------
36 | markets : string, default "EQUITY", optional
37 | Desired market for retrieval (EQUITY, OPTION, FUTURE, BOND,
38 | or FOREX)
39 | date : datetime.datetime object, optional
40 | Data to retrieve hours for (defaults to current day)
41 | output_format: str, optional, default 'pandas'
42 | Desired output format (json or Pandas DataFrame).
43 |
44 | .. note:: JSON output formatting only if "FUTURE" is selected.
45 | api: pyTD.api.api object, optional
46 | A pyTD api object. If not passed, API requestor defaults to
47 | pyTD.api.default_api
48 | """
49 | _MARKETS = {"equity": "EQ",
50 | "option": "EQO",
51 | "future": None,
52 | "bond": "BON",
53 | "forex": "forex"}
54 |
55 | def __init__(self, markets="EQUITY", date=None, output_format='pandas',
56 | api=None):
57 | self.date = date or datetime.datetime.now()
58 | err_msg = "Please enter one more most markets (EQUITY, OPTION, etc.)"\
59 | "for retrieval."
60 | self.markets = _handle_lists(markets, err_msg=err_msg)
61 | self.markets = [market.lower() for market in self.markets]
62 | if not set(self.markets).issubset(set(self._MARKETS)):
63 | raise ValueError("Please input valid markets for hours retrieval.")
64 | super(MarketHours, self).__init__(output_format, api)
65 |
66 | @property
67 | def params(self):
68 | return {
69 | "markets": ','.join(self.markets),
70 | "date": self.date.strftime('%Y-%m-%d')
71 | }
72 |
73 | @property
74 | def resource(self):
75 | return 'hours'
76 |
77 | def _convert_output(self, out):
78 | data = {market: out[market][self._MARKETS[market]] for market in
79 | self.markets}
80 | return pd.DataFrame(data)
81 |
--------------------------------------------------------------------------------
/docs/source/auth.rst:
--------------------------------------------------------------------------------
1 | .. _auth:
2 |
3 | Authentication
4 | ==============
5 |
6 | TD Ameritrade uses `OAuth 2.0 `__ to authorize and
7 | authenticate requests.
8 |
9 |
10 | .. seealso:: Not familiar with OAuth 2.0? See :ref:`What is OAuth 2.0?` for an overview of OAuth Authentication and Authorization.
11 |
12 | .. _auth.overview:
13 |
14 | Overview
15 | --------
16 |
17 | 1. Send Consumer Key and Callback URL from your app's details to TD Ameritrade
18 | 2. Open web browser to TD Ameritrade, **login to TD Ameritrade Brokerage Account**
19 | 3. Send authorization code to receive refresh and access tokens
20 | 4. Refresh and access tokens are stored in your ``api`` instance's ``cache`` (either ``DiskCache`` or ``MemCache``)
21 |
22 | .. _auth.script:
23 |
24 | Script Application
25 | ------------------
26 |
27 | **Script** applications are the simplest type of application to work with
28 | because they don't involve any sort of callback process to obtain an
29 | ``access_token``.
30 |
31 | TD Ameritrade requires that you provide a Callback URL when registering your application -- ``http://localhost:8080`` is a simple one to use.
32 |
33 | .. seealso:: :ref:`What should my Callback URL be?`
34 |
35 |
36 | pyTD provides a simple web server, written in pure Python, to handle
37 | authentication with TD Ameritrade. If used for authentication, this server will run on your localhost (127.0.0.1) and receive your authorization code at your specified Callback URL.
38 |
39 | .. _auth.web:
40 |
41 | Web Application
42 | ---------------
43 |
44 | If you have a **web** application and want to be able to access pyTD
45 | Enter the appropriate Callback URL and configure that endpoint to complete the code flow.
46 |
47 |
48 | .. _auth.installed:
49 |
50 | Installed Application
51 | ---------------------
52 |
53 |
54 |
55 | .. _auth.cache:
56 |
57 | Token Caching
58 | -------------
59 |
60 | .. warning:: To enable persistent access to authentication tokens across sessions, pyTD stores tokens on-disk by default. **Storing tokens on-disk may pose a security risk to some users.** See :ref:`Is it safe to save my authentications on-disk?` for more information.
61 |
62 | By default, tokens are stored *on-disk* in the :ref:`Configuration
63 | Directory`, though they can also be stored *in-memory*. There are two ways to select a token storage method:
64 |
65 | 1. **Environment Variable** (recommended) - set the ``TD_STORE_TOKENS`` variable:
66 |
67 | .. code-block:: bash
68 |
69 | $ export TD_STORE_TOKENS=False
70 |
71 | 2. Pass ``store_tokens`` keyword argument when creating an ``api`` instance to set token storage temporarily:
72 |
73 | .. code-block:: python
74 |
75 | from pyTD.api import api
76 |
77 | oid = "TEST@AMER.OAUTHAP"
78 | uri = "https://localhost:8080"
79 |
80 | a = api(consumer_key=oid, callback_url=uri, store_tokens=False)
81 |
82 | When ``store_tokens`` is set to ``False``, any stored tokens will be freed from memory when the program exits.
83 |
84 |
85 | Caches
86 | ~~~~~~
87 |
88 | In-Memory - ``MemCache``
89 | ^^^^^^^^^^^^^^^^^^^^^^^^
90 |
91 | The ``MemCache`` class provides in-memory caching for authorization tokens.
92 |
93 | **Important** - the stored tokens will be freed from memory when your program exits.
94 |
95 | .. autoclass:: pyTD.cache.MemCache
96 |
97 |
98 | On-Disk - ``DiskCache``
99 | ^^^^^^^^^^^^^^^^^^^^^^^
100 |
101 | To store auth tokens on-disk, the ``DiskCache`` class is provided. When passed an absolute path, ``DiskCache`` creates the necessary directories and instantiates an empty cache file.
102 |
103 | .. autoclass:: pyTD.cache.DiskCache
104 |
105 |
106 | .. todo:: ``SQLCache`` - caching auth tokens in a sqllite database.
107 |
108 |
109 |
110 |
111 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 | import codecs
3 | import os
4 | import re
5 |
6 | here = os.path.abspath(os.path.dirname(__file__))
7 |
8 |
9 | def find_version(*file_paths):
10 | """Read the version number from a source file.
11 | Why read it, and not import?
12 | see https://groups.google.com/d/topic/pypa-dev/0PkjVpcxTzQ/discussion
13 | """
14 | # Open in Latin-1 so that we avoid encoding errors.
15 | # Use codecs.open for Python 2 compatibility
16 | with codecs.open(os.path.join(here, *file_paths), 'r', 'latin1') as f:
17 | version_file = f.read()
18 |
19 | # The version line must have the form
20 | # __version__ = 'ver'
21 | version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]",
22 | version_file, re.M)
23 | if version_match:
24 | return version_match.group(1)
25 | raise RuntimeError("Unable to find version string.")
26 |
27 |
28 | def parse_requirements(filename):
29 |
30 | with open(filename) as f:
31 | required = f.read().splitlines()
32 | return required
33 |
34 |
35 | # Get the long description from the relevant file
36 | with codecs.open('README.rst', encoding='utf-8') as f:
37 | long_description = f.read()
38 |
39 | setup(
40 | name="pyTD",
41 | version=find_version('pyTD', '__init__.py'),
42 | description="Python interface to TD Ameritrade Developer API",
43 | long_description=long_description,
44 |
45 | # The project URL.
46 | url='https://github.com/addisonlynch/pyTD',
47 | download_url='https://github.com/addisonlynch/pyTD/releases',
48 |
49 | # Author details
50 | author='Addison Lynch',
51 | author_email='ahlshop@gmail.com',
52 | test_suite='pytest',
53 |
54 | # Choose your license
55 | license='Apache',
56 |
57 | classifiers=[
58 | # How mature is this project? Common values are
59 | # 3 - Alpha
60 | # 4 - Beta
61 | # 5 - Production/Stable
62 | 'Development Status :: 3 - Alpha',
63 |
64 | # Indicate who your project is intended for
65 | 'Intended Audience :: Developers',
66 | 'Intended Audience :: Financial and Insurance Industry',
67 | 'Topic :: Office/Business :: Financial :: Investment',
68 | 'Topic :: Software Development :: Libraries :: Python Modules',
69 | 'Operating System :: OS Independent',
70 |
71 | # Pick your license as you wish (should match "license" above)
72 | 'License :: OSI Approved :: Apache Software License',
73 |
74 | # Specify the Python versions you support here. In particular, ensure
75 | # that you indicate whether you support Python 2, Python 3 or both.
76 | 'Programming Language :: Python',
77 | 'Programming Language :: Python :: 2.7',
78 | 'Programming Language :: Python :: 3.4',
79 | 'Programming Language :: Python :: 3.5',
80 | 'Programming Language :: Python :: 3.6'
81 | ],
82 |
83 | # What does your project relate to?
84 | keywords='stocks market finance tdameritrade quotes shares currency',
85 |
86 | # You can just specify the packages manually here if your project is
87 | # simple. Or you can use find_packages.
88 | packages=find_packages(exclude=["contrib", "docs", "tests*"]),
89 |
90 | # List run-time dependencies here. These will be installed by pip when your
91 | # project is installed.
92 | install_requires=parse_requirements("requirements.txt"),
93 | setup_requires=['pytest-runner'],
94 | tests_require=parse_requirements("requirements-dev.txt"),
95 | # If there are data files included in your packages that need to be
96 | # installed, specify them here. If using Python 2.6 or less, then these
97 | # have to be included in MANIFEST.in as well.
98 | package_data={
99 | 'pyTD': [],
100 | },
101 |
102 |
103 | )
104 |
--------------------------------------------------------------------------------
/pyTD/tests/unit/test_instruments.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import pandas as pd
24 | import pytest
25 |
26 | from pyTD.tests.test_helper import pyTD
27 |
28 | ResourceNotFound = pyTD.utils.exceptions.ResourceNotFound
29 | TDQueryError = pyTD.utils.exceptions.TDQueryError
30 |
31 |
32 | @pytest.mark.webtest
33 | class TestInstrument(object):
34 |
35 | def test_instrument_no_symbol(self):
36 | with pytest.raises(TypeError):
37 | pyTD.instruments.get_instrument()
38 |
39 | def test_instrument_bad_instrument(self):
40 | with pytest.raises(ResourceNotFound):
41 | pyTD.instruments.get_instrument("BADINSTRUMENT")
42 |
43 | def test_instrument_cusip(self):
44 | cusip = "68389X105"
45 | data = pyTD.instruments.get_instrument(cusip,
46 | output_format='json')[cusip]
47 |
48 | assert isinstance(data, dict)
49 |
50 | assert data["symbol"] == "ORCL"
51 | assert data["exchange"] == "NYSE"
52 |
53 | def test_instruments_cusip(self):
54 | cusip = "17275R102"
55 | data = pyTD.instruments.get_instruments(cusip,
56 | output_format='json')[cusip]
57 |
58 | assert isinstance(data, dict)
59 |
60 | assert data["symbol"] == "CSCO"
61 | assert data["exchange"] == "NASDAQ"
62 |
63 | def test_instrument_cusp_pandas(self):
64 | data = pyTD.instruments.get_instrument("68389X105").T
65 |
66 | assert isinstance(data, pd.DataFrame)
67 |
68 | assert len(data) == 1
69 | assert len(data.columns) == 5
70 | assert data.iloc[0]["symbol"] == "ORCL"
71 |
72 | def test_instrument_symbol(self):
73 | data = pyTD.instruments.get_instrument("AAPL").T
74 |
75 | assert isinstance(data, pd.DataFrame)
76 |
77 | assert len(data) == 1
78 | assert len(data.columns) == 5
79 | assert data.iloc[0]["symbol"] == "AAPL"
80 |
81 | def test_instruments_fundamental(self):
82 | data = pyTD.instruments.get_instruments("AAPL",
83 | projection="fundamental").T
84 |
85 | assert isinstance(data, pd.DataFrame)
86 |
87 | assert len(data.columns) == 46
88 | assert data.iloc[0]["symbol"] == "AAPL"
89 |
90 | def test_instruments_regex(self):
91 | data = pyTD.instruments.get_instruments("AAP.*",
92 | projection="symbol-regex").T
93 |
94 | assert isinstance(data, pd.DataFrame)
95 |
96 | assert data.shape == (13, 5)
97 |
98 | def test_instruments_bad_projection(self):
99 | with pytest.raises(TDQueryError):
100 | pyTD.instruments.get_instruments("AAPL",
101 | projection="BADPROJECTION")
102 |
--------------------------------------------------------------------------------
/pyTD/utils/__init__.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import logging
24 | import requests
25 | import datetime as dt
26 | import time
27 |
28 | import pandas as pd
29 | from pandas import to_datetime
30 | import pandas.compat as compat
31 |
32 | from pyTD.compat import is_number
33 |
34 |
35 | logger = logging.getLogger(__name__)
36 |
37 |
38 | def bprint(msg):
39 | return color.BOLD + msg + color.ENDC
40 |
41 |
42 | class color:
43 | HEADER = '\033[95m'
44 | OKBLUE = '\033[94m'
45 | OKGREEN = '\033[92m'
46 | WARNING = '\033[93m'
47 | FAIL = '\033[91m'
48 | ENDC = '\033[0m'
49 | BOLD = '\033[1m'
50 | UNDERLINE = '\033[4m'
51 | GRN = '\x1B[32m'
52 | RED = '\x1B[31m'
53 |
54 |
55 | def gprint(msg):
56 | return(color.GRN + msg + color.ENDC)
57 |
58 |
59 | def _handle_lists(l, mult=True, err_msg=None):
60 | if isinstance(l, (compat.string_types, int)):
61 | return [l] if mult is True else l
62 | elif isinstance(l, pd.DataFrame) and mult is True:
63 | return list(l.index)
64 | elif mult is True:
65 | return list(l)
66 | else:
67 | raise ValueError(err_msg or "Only 1 symbol/market parameter allowed.")
68 |
69 |
70 | def _init_session(session):
71 | if session is None:
72 | session = requests.session()
73 | return session
74 |
75 |
76 | def input_require(msg):
77 | result = ''
78 | while result == '':
79 | result = input(msg)
80 | return result
81 |
82 |
83 | def rprint(msg):
84 | return(color.BOLD + color.RED + msg + color.ENDC)
85 |
86 |
87 | def _sanitize_dates(start, end, set_defaults=True):
88 | """
89 | Return (datetime_start, datetime_end) tuple
90 | if start is None - default is 2017/01/01
91 | if end is None - default is today
92 |
93 | Borrowed from Pandas DataReader
94 | """
95 | if is_number(start):
96 | # regard int as year
97 | start = dt.datetime(start, 1, 1)
98 | start = to_datetime(start)
99 |
100 | if is_number(end):
101 | end = dt.datetime(end, 1, 1)
102 | end = to_datetime(end)
103 |
104 | if set_defaults is True:
105 | if start is None:
106 | start = dt.datetime(2017, 1, 1, 0, 0)
107 | if end is None:
108 | end = dt.datetime.today()
109 | if start and end:
110 | if start > end:
111 | raise ValueError('start must be an earlier date than end')
112 | return start, end
113 |
114 |
115 | def to_timestamp(date):
116 | return int(time.mktime(date.timetuple()))
117 |
118 |
119 | def yn_require(msg):
120 | template = "{} [y/n]: "
121 | result = ''
122 | YES = ["y", "Y", "Yes", "yes"]
123 | NO = ["n", "N", "No", "no"]
124 | while result not in YES and result not in NO:
125 | result = input_require(template.format(msg))
126 | if result in YES:
127 | return True
128 | if result in NO:
129 | return False
130 |
--------------------------------------------------------------------------------
/github_deploy_key_addisonlynch_pytd.enc:
--------------------------------------------------------------------------------
1 | gAAAAABbzTue7_Slkc3SDo7xdI-e5qH654164ieKRxLiJ1b9I1awfX1EsP6Lxtgl2dOUNWmnjPLp28vx_jyst9Rh_oT9uc6CrQ1EU7-MdQiH13_0C4oHTgrQYFZC7E0i25O2LsEFmhiXcqKb_LK2EBRmD_jZ4O3v-3UVdPe6nHJCidsHR5F0vTUYcQOsLtFB6McA8Jm0ax_LXkFpSHzVRtHJlaubtX7o55tfkvqooE8bRJeaGgNhVYV4DTK4dL8ftd8NgseATeUqd1fcANW89OHW2yzN8XpGVKdam3o6yIgrzL9wIZBC0nSYsfoYdNbsVWz7lJroxe2KtiY5VDyJlvnhu3MWAhQQtlpw-kFouQyrmpjzUC_dTpN8izFVabmTAJeBwLsbRQcMC300p7q05Ir6VYk4aMahtRKXeMixLT6hB3c_9IMqzD5cQKTMAPrh6TC0rOfWQpo87ldKg6FCDm8AcNIBbXLwau_XqhSZ56YJ2maBCdIiyW5bE-W9GaLkW5OEHu__QvPBoXDPXQqFKcuPaPxzJ2TI57DBZmszGoceCBueOZfoPmFn-h7GhafQBu7WDRfMwD7cIN_gEsJRqQD6b3F-yPosZsq1hMnh97yWi5Wvye_wjj1of00MkNavV9ZQSUYPh2ttp2aylT2kmP8eYLq2vqqUVJ9b-bl7shAia-YmW1vcDV1iOGWrctfuGEHiWzVZ3LMlDR0EW1hIxdyIhKeIiUMggPYqCTtlBwZLf5nzFF-4FbV2l1SDhrY8oJIdaPE3asRCevxS8tc4WjkRC0lfKuHrGAIvOXSC-4-6xFDfCcImLGEGawDz0-wjnKZIs7NwxVLpU1-_tXVQAZgiUwizXx7LR1N7bVX_yk7JNNjBVpVMADIyYsSGkapjLGY8IgQnERc2l02GU2yRJoRRSAmEwbuII7NlRRDfPUYCOQWvKzvRK1ar6VZtUe7UdLCTl5teXpz0ucZ8Ktu2APuLugOm9AEKCW_wNR9ryGQ1FojB4Wh2f_-11u0yVt8Bz-UO_OZApkcB165OiERetemo-STsGjmWjxbzbBLQ4EryLiyzEYRa3cDUWcZTBhhMrCwT-m7XORJcOfU-4gXJ5p_aUQaXNxhZCg4YJDmjaAZi0SXd-ciKox0CRQxFyNlEzqrYpUE9QMAOA0kSB9HsE_oU9Qpdrk0aEr7VBfWMUpACovtQgn_n2DpQF8xdD6MxqS_3VUEAnkQPG8Xi4xwXN9ENaFaJ9p1PFEoLdWPVv_Jvyc3K60xKUXJRXUlnVc4U3zE64pXwBnSF3BQrgVJPQObZzCVJcKdRHa8xRgovqll8H6QsoiHkpvd6YHh3WCvMXbq2a5iVvSnPijICna13XJfRuc4OX8doBk3W8jVlrLWj3k6ajkdq9B_5qjwCl_J-bPZIZLoK8SmosB9Icim73jmGDTzbfLQJ_ylRh-_SjOidVCZIEACStdhkTkHpGZwa2JwIb0j6yVF0vhXuvlgXoLnbsUNoRIuqQ-xYY3QYRtLpW82z38PluXfDlL2pMPZGGGoKDVcZOqSW5RAa3gev3ZjSEitS7MgXflOLjrKZKeA-SikbQ4xVN3hpWeuHXXReOaJfjOsTi2CGYMULfjP1coAw-INWoo4BJqpSkL3kxiw9vfJGfozGOdyFwkT17FIVwBvICSsfrBmtoT0_J_suNuVNQNqYHX6M-ec20dq33nm6LEdBMJ_SP4xzV7QYFLMbWhzrSTVqbMdtmj6XGCFipJKVF32yjKzZHfdVkckJeUPjByXbdCsOijTacQWM_BUYMJyv_p9fsXrc8WP1jhfff9ICaQqhUcugK1kETlV4lqidQnPjrZd-o-TdI-a6RXobX_v5-cm0soC5s0GatPS0rnryemzmzpypAEPvv13Z41JzVygl2EstrA6ZhCVxebHh40rMSwRFp4zhJMht2SvIBVH8WdocXrKNTzl6EIAJC8KUj72yfRz6_xm_wOMNkTstkvC35ANDqj8BZY0HejNcocdN_zXzbKq3I4Ru1s4EoqtNrv7Xa-uLfp2-MSYEmMQrYBbu1-P9Ln833uaDILYw6oEOWDH0nKWx_9-HWBDFn2G9JGUTSA8mfnILEx9I74j-2Unv7fBciLWjdMErEGmwOnIMIr0ajg045PWUxvfshMROpWs5oDdcVKfO9Pw5MmSHDJka19g1DeS3fhGmt-ejwpJHO9w6KO1gcE2W1UH5S_N4aXFIcAS8FNnP_7BV0796bpQVS_j49tv2L2gTbx7y-3qHLY_Cnk2y5m4h7jAxIyx5H-PxdHM3Z9qg9U4nU7wtwQOs5Z5AhahNoH18yNE3geod6-F6EG6aY2UjOe42tPrIsxTD3lMkBx6bdmyitsXnDU-Xsj60_vH62rW9RNuZUpkTaGdaitsBRYT1QMDQu_X6tUZiklcOqFXjKKHP6RCQFUwtlIvvE3Y40SAW4BfLGWOFbrFKkjuYNxm4BMFh5QaY4bBoq_LYL8gg1djribTdnLcUucirEGU3CGVlM3wTOtJvzGGBrdeCOlYFnzTBvzbQs8VPJy_4TWxWqUirqyFKjP6Fjkhz4pXyex4OrfQhIkPX35rwv0uvn-fAcEZbXUZv0SO8nt0ko9A8EClCbcewmIByDTfMxGHHVagA_0gx8Nby-6YXQ_bGI_pXNyW-pVOyXTMwZU8fpVtfHbgE3vVYDtWi17ZEsa42fALu9KYDnV1ElgimBwT0ASID4O9RWakcvsD5yphTuz9gBpRFfINozwkUV7WXAqgdJsXygYaCF0PSnKTAVBiyaXmNhLNKAQfFCzQVwJtMHScIN-9EvqrmlFCDhPhWVB68bMH6EoVduaCokTPYoqP3WEkyqN8eloiabdGBpIM4kZSX6aX4O8HJMgpActer1hwpgQZXiShMjYmgWiwjEnQtPRr4nArtCD4RO2KhtDsHRJfT-XMBx2e8faQh0cSCi4LQd6AhUT-787pvanmdPbhpg4gqVOJaKWNd0ggOhlcIz3meaw4U7bPDY5seAltQJlMS89kyZQUuRJYjuJQrSPuiH7yklggMnmmCT--uAAEr2w_jtVLSAWGkEomtsFmLe0n4_CDxXr8-wWdQ3XqqgYC4-QOqSoeNDI8ggrp9hxy50ysWIdATgwIVbYlbmqpHy55XwTOzE8TYwsSPsxlwHuBe20_fea6U64-7kDLU4U56za49aCRV3DH4Mmc3ZciDZRO1FWmL_TctJq628d2pgYdV9CrZHdVtJlji7sDtqO3hRT1NMwjawdtmU7qeruh_hRZjLfyNo6GsKKMgd9s_42V8-g8EKLkR7as6tH1ANytHlQRhENajAryEQ89_ZoPTyfyCGShL04EcsZJV6EGUIWUAK0Mv0PTU3_tNV8iea_SWh7A-IlSfP92QClBQNm2zCNBA-J9UCZC0R5SfMVkXntJtseA1b12bF_M-nvIdLyVi-pk3l4TkiWwVcULH28Pl3KA7jJ-W8eroWNcD4dZu1BTOYsG0fONoHItjOTScpxaNvWqMVs1eUW1gOFwnNogJcMJRb70leRiwXlcyrZ3VXqqtn63bOA-tYq87TC7IANgRu9sJ5tyKm6-a7DKncmUq8EcWdim4MtS9ody8sU1-7cQ0yLF36LQGLDlg1u9RWJ-_9T--nGgzqTF7qjN3KN2YgALGW0JtQ4ExRolnmTRefJzmi424aijscDtWVKw2hlpnNoE8BxBVXgkeczd3qC0XjW7GbP3ic6nAIXb7UovpjBg6ADPPqH9P2-smjCQcUHvQVMo-EM7CSQmcZ1inr_o9FwxQboKtfjaNgiKF9XihTTbkIqc6yToSnWDIAXFXCjBUp7Tfn9nBS3xku-XUuuV253-8ARawtgexVI_qTHCaDaIox-2Nn_DnNjaQKsTPw8mJyElpqa4gABUtBsun70hDzF4Rl-ix3i_eFi7ZZDI9htvrj73WzU1_8B-Zc1S3oBQY-oILn0JnSqIwdG7oa8pEE4g25XdRwPjQXY3LU17No5uC8cuxAI1k9TQWcjmsS1Kug6hjpbsChYgBd7pkP4qZBjTqGLm6ou_82LDsaL05wiExHF9IcnMfARpdhA1u0WG2U1XRTCzntDj2iq3UQ551Vr0YOM9o-f7Ki8scOY16_9VVyFKxEHFGKQl9PEi256m9OnHAsXzDKZtK7EEwuNLgUr0iKHhfEaGvlt4Bx-ZAwWZmpYp7EAi46Ga127CbZv_cVhyDyBOpFTrECH_ybOIfmBWsYaawyG9kZDZnCio-kfmvFBXyGcjGsz5aJUsf4VKF166Fe52BdXnBg1clTuGfxKdJK0mDWSFztTUDwLfi_s_Jy-xEv0WyXeR4WbRqvlLHBoKqz6qsKTttxtyS1NfopjzIZ_JbUTeytQW-5ooCWqbOm_2dEGjEXdWIAZinHbMwWV_6yg2S5GHbjzzDVKD9cnLRanXk3NzE-wdZETX4jWuBaA==
--------------------------------------------------------------------------------
/pyTD/tests/unit/test_auth.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import json
24 | import pytest
25 | from pyTD.compat import MagicMock
26 |
27 | from pyTD.auth import TDAuthManager
28 | from pyTD.cache import MemCache
29 | from pyTD.auth.server import TDAuthServer
30 | from pyTD.auth.tokens import EmptyToken, RefreshToken, AccessToken
31 | from pyTD.utils.exceptions import AuthorizationError
32 | from pyTD.utils.testing import MockResponse
33 |
34 |
35 | @pytest.fixture(scope='function', autouse=True)
36 | def sample_manager(sample_oid, sample_uri, valid_cache):
37 | def dummy(*args, **kwargs):
38 | return None
39 | c = MemCache()
40 | m = TDAuthManager(c, sample_oid, sample_uri)
41 | m._open_browser = dummy
42 | m._start_auth_server = dummy
43 | m._stop_auth_server = dummy
44 | return m
45 |
46 |
47 | @pytest.fixture(scope='function')
48 | def test_auth_response():
49 | return {
50 | "refresh_token": "TESTREFRESHVALUE",
51 | "refresh_token_expires_in": 7776000,
52 | "access_token": "TESTACCESSVALUE",
53 | "expires_in": 1800,
54 | "access_time": 1534976931
55 | }
56 |
57 |
58 | @pytest.fixture(scope='function')
59 | def test_auth_response_bad():
60 | return {
61 | "error": "Bad request"
62 | }
63 |
64 |
65 | class TestAuth(object):
66 |
67 | def test_auth_init(self, sample_manager):
68 |
69 | assert isinstance(sample_manager.refresh_token, EmptyToken)
70 | assert isinstance(sample_manager.access_token, EmptyToken)
71 |
72 | def test_refresh_access_token_no_refresh(self, sample_manager):
73 |
74 | with pytest.raises(AuthorizationError):
75 | sample_manager.refresh_access_token()
76 |
77 | def test_auth_browser_fails(self, sample_manager, test_auth_response_bad):
78 | mock_server = MagicMock(TDAuthServer)
79 | mock_server._wait_for_tokens.return_value = test_auth_response_bad
80 |
81 | sample_manager.auth_server = mock_server
82 |
83 | with pytest.raises(AuthorizationError):
84 | sample_manager.auth_via_browser()
85 |
86 | def test_auth_browser_succeeds(self, sample_oid, sample_uri,
87 | sample_manager, monkeypatch,
88 | test_auth_response,
89 | valid_refresh_token,
90 | valid_access_token):
91 | mock_server = MagicMock(TDAuthServer)
92 | mock_server._wait_for_tokens.return_value = test_auth_response
93 |
94 | sample_manager.auth_server = mock_server
95 |
96 | r1, r2 = sample_manager.auth_via_browser()
97 | assert isinstance(r1, RefreshToken)
98 | assert isinstance(r2, AccessToken)
99 | assert r1.token == "TESTREFRESHVALUE"
100 | assert r2.token == "TESTACCESSVALUE"
101 |
102 | def test_auth_refresh_access_bad_token(self, invalid_cache, monkeypatch,
103 | mock_400, sample_oid, sample_uri):
104 | c = invalid_cache
105 | c.refresh_token.expires_in = 100000000000
106 | monkeypatch.setattr("pyTD.auth.manager.requests.post", lambda *a, **k:
107 | mock_400)
108 |
109 | manager = TDAuthManager(c, sample_oid, sample_uri)
110 | with pytest.raises(AuthorizationError):
111 | manager.refresh_access_token()
112 |
113 | def test_auth_refresh_access(self, test_auth_response, monkeypatch,
114 | sample_oid, sample_uri, valid_cache):
115 |
116 | mocked_response = MockResponse(json.dumps(test_auth_response), 200)
117 | monkeypatch.setattr("pyTD.auth.manager.requests.post", lambda *a, **k:
118 | mocked_response)
119 |
120 | manager = TDAuthManager(valid_cache, sample_oid, sample_uri)
121 | manager.refresh_access_token()
122 | assert manager.access_token.token == "TESTACCESSVALUE"
123 |
--------------------------------------------------------------------------------
/pyTD/tests/unit/test_api.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import pytest
24 | import subprocess
25 |
26 | from pyTD.api import api, default_api, gen_ssl
27 | from pyTD.utils.exceptions import (SSLError, ConfigurationError,
28 | ValidationError, AuthorizationError,
29 | ForbiddenAccess, ResourceNotFound,
30 | ClientError, ServerError)
31 | from pyTD.utils.testing import MockResponse
32 |
33 |
34 | @pytest.fixture(params=[
35 | (400, ValidationError),
36 | (401, AuthorizationError),
37 | (403, ForbiddenAccess),
38 | (404, ResourceNotFound),
39 | (450, ClientError),
40 | (500, ServerError)
41 | ])
42 | def bad_requests(request):
43 | return request.param
44 |
45 |
46 | class TestAPI(object):
47 |
48 | def test_non_global_api(self, sample_oid, sample_uri):
49 |
50 | a = api(consumer_key=sample_oid, callback_url=sample_uri)
51 |
52 | assert a.consumer_key == sample_oid
53 | assert a.callback_url == sample_uri
54 |
55 | assert a.refresh_valid is False
56 | assert a.access_valid is False
57 | assert a.auth_valid is False
58 |
59 | def test_api_passed_dict(self, sample_oid, sample_uri, valid_cache):
60 | params = {
61 | "consumer_key": sample_oid,
62 | "callback_url": sample_uri,
63 | "cache": valid_cache
64 | }
65 |
66 | a = api(params)
67 |
68 | assert a.consumer_key == sample_oid
69 | assert a.callback_url == sample_uri
70 |
71 | assert a.refresh_valid is True
72 | assert a.access_valid is True
73 | assert a.auth_valid is True
74 |
75 |
76 | class TestDefaultAPI(object):
77 |
78 | def test_default_api(self, sample_oid, sample_uri, set_env):
79 |
80 | a = default_api(ignore_globals=True)
81 |
82 | assert a.consumer_key == sample_oid
83 | assert a.callback_url == sample_uri
84 |
85 | assert a.refresh_valid is False
86 | assert a.access_valid is False
87 | assert a.auth_valid is False
88 |
89 | def test_default_api_no_env(self, del_env):
90 |
91 | with pytest.raises(ConfigurationError):
92 | default_api(ignore_globals=True)
93 |
94 |
95 | class sesh(object):
96 |
97 | def __init__(self, response):
98 | self.response = response
99 |
100 | def request(self, *args, **kwargs):
101 | return self.response
102 |
103 |
104 | class TestAPIRequest(object):
105 |
106 | def test_api_request_errors(self, bad_requests):
107 | mockresponse = MockResponse("Error", bad_requests[0])
108 | m_api = default_api(ignore_globals=True)
109 |
110 | m_api.session = sesh(mockresponse)
111 |
112 | with pytest.raises(bad_requests[1]):
113 | m_api.request("GET", "https://none.com")
114 |
115 | def test_bad_oid_request(self):
116 | mockresponse = MockResponse('{"error": "Invalid ApiKey"}', 500)
117 | api = default_api(ignore_globals=True)
118 |
119 | api.session = sesh(mockresponse)
120 |
121 | with pytest.raises(AuthorizationError):
122 | api.request("GET", "https://none.com")
123 |
124 |
125 | class TestGenSSL(object):
126 |
127 | def test_gen_ssl_pass(self, monkeypatch):
128 | monkeypatch.setattr("pyTD.api.subprocess.check_call",
129 | lambda *a, **k: True)
130 | monkeypatch.setattr("pyTD.api.os.chdir", lambda *a, **k: None)
131 | assert gen_ssl(".") is True
132 |
133 | @pytest.mark.skip
134 | def test_gen_ssl_raises(self, monkeypatch):
135 | def mocked_check_call(*args, **kwargs):
136 | raise subprocess.CalledProcessError(1, "openssl")
137 |
138 | monkeypatch.setattr("pyTD.api.subprocess.check_call",
139 | mocked_check_call)
140 | monkeypatch.setattr("pyTD.api.os.chdir", lambda *a, **k: None)
141 |
142 | with pytest.raises(SSLError):
143 | gen_ssl("/path/to/dir")
144 |
--------------------------------------------------------------------------------
/docs/source/market.rst:
--------------------------------------------------------------------------------
1 | .. _market:
2 |
3 |
4 | Market Data
5 | ===========
6 |
7 | TD Ameritrade provides various endpoints to obtain Market Data for various instruments and markets across asset classes.
8 |
9 | **Endpoints**
10 |
11 | 1. :ref:`Quotes `
12 | 2. :ref:`Market Movers `
13 | 3. :ref:`Market Hours `
14 | 4. :ref:`Option Chains `
15 | 5. :ref:`Price History `
16 | 6. :ref:`Fundamentals `
17 |
18 |
19 | .. _market.quotes:
20 |
21 | Quotes
22 | ------
23 |
24 | The `Get Quote
25 | `__
26 | and `Get Quotes
27 | `__
28 | endpoints provide real-time and delayed quotes. Access is provided by pyTD
29 | through the top-level function ``get_quotes``, which combines functionality of
30 | the two endpoints.
31 |
32 |
33 | .. autofunction:: pyTD.market.get_quotes
34 |
35 | .. _market.quotes-examples:
36 |
37 | Examples
38 | ~~~~~~~~
39 |
40 | **Single Stock**
41 |
42 | .. ipython:: python
43 |
44 | from pyTD.market import get_quotes
45 |
46 | get_quotes("AAPL").head()
47 |
48 |
49 | **Multiple Stocks**
50 |
51 | .. ipython:: python
52 |
53 | get_quotes(["AAPL", "TSLA"]).head()
54 |
55 |
56 |
57 | .. _market.movers:
58 |
59 | Movers
60 | ------
61 |
62 | The `Get Movers `__ endpoint provides market movers (up or down) for a specified index. Access is provided by pyTD through the top-level function ``get_movers``.
63 |
64 | **Format** - 'json' (dictionary) or 'pandas' (Pandas DataFrame)
65 |
66 | .. autofunction:: pyTD.market.get_movers
67 |
68 | .. note:: The desired index should be prefixed with ``$``. For instance, the Dow Jones Industrial Average is ``$DJI``.
69 |
70 | .. warning:: This endpoint may return empty outside of Market Hours.
71 |
72 | .. _market.movers-examples:
73 |
74 | Examples
75 | ~~~~~~~~
76 |
77 | .. ipython:: python
78 |
79 | from pyTD.market import get_movers
80 |
81 | get_movers("$DJI")
82 |
83 |
84 | .. _market.hours:
85 |
86 | Hours
87 | -----
88 |
89 | The `Get Market Hours
90 | `__
91 | endpoint provides market hours for various markets, including equities,
92 | options, and foreign exchange (forex). Access is provided by pyTD through the top-level function ``get_market_hours``.
93 |
94 | By default, ``get_market_hours`` returns the market hours of the current date,
95 | but can do so for any past or future date when passed the optional keyword argument ``date``.
96 |
97 | .. autofunction :: pyTD.market.get_market_hours
98 |
99 | .. _market.hours-examples:
100 |
101 | Examples
102 | ~~~~~~~~
103 |
104 | .. ipython:: python
105 |
106 | from pyTD.market import get_market_hours
107 |
108 | get_market_hours("EQUITY")
109 |
110 |
111 | .. _market.option-chains:
112 |
113 | Option Chains
114 | -------------
115 |
116 | The `Get Option Chains `__ endpoint provides option chains for optionable equities symbols. Access is provided by pyTD through the top-level function ``get_option_chains``.
117 |
118 | ``get_option_chains`` accepts a variety of arguments, which allow filtering of the results by criteria such as strike price, moneyness, and expiration date, among others. Futher, it is possible to specify certain parameters to be used in calculations for analytical strategy chains.
119 |
120 |
121 | .. autofunction :: pyTD.market.get_option_chains
122 |
123 | .. _market.option-chains-examples:
124 |
125 | Examples
126 | ~~~~~~~~
127 |
128 | Simple
129 | ^^^^^^
130 |
131 | .. ipython:: python
132 |
133 | from pyTD.market import get_option_chains
134 |
135 | get_option_chains("AAPL")
136 |
137 | .. _market.price-history:
138 |
139 | Historical Prices
140 | -----------------
141 |
142 | The `Get Price History `__ endpoint provides historical pricing data for symbols across asset classes. Access is provided by pyTD through the top-level function ``get_price_history``.
143 |
144 |
145 | .. autofunction :: pyTD.market.get_price_history
146 |
147 | .. _market.price-history-examples:
148 |
149 | Examples
150 | ~~~~~~~~
151 |
152 | .. ipython:: python
153 |
154 | import datetime
155 | from pyTD.market import get_price_history
156 |
157 | start = datetime.datetime(2017, 1, 1)
158 | end = datetime.datetime(2018, 1, 1)
159 |
160 | get_price_history("AAPL", start_date=start, end_date=end).head()
161 |
162 |
163 | .. _market.fundamentals:
164 |
165 | Fundamental Data
166 | ----------------
167 |
168 | Fundamental data can also be accesed through ``get_fundamentals``, which wraps
169 | ``pyTD.instruments.get_instruments`` for convenience.
170 |
171 | .. ipython:: python
172 |
173 | from pyTD.market import get_fundamentals
174 |
175 | get_fundamentals("AAPL").head()
176 |
--------------------------------------------------------------------------------
/pyTD/cache/disk_cache.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import json
24 | import logging
25 | import os
26 |
27 | from pyTD.auth.tokens import AccessToken, RefreshToken
28 | from pyTD.cache.base import TokenCache
29 | from pyTD.utils.exceptions import ConfigurationError
30 |
31 | logger = logging.getLogger(__name__)
32 |
33 |
34 | class DiskCache(TokenCache):
35 | """
36 | On-disk token cache for access and refresh tokens
37 |
38 | Attributes
39 | ----------
40 | config_dir: str
41 | Desired directory to store cache
42 | filename: str
43 | Desired cache file name
44 |
45 | Usage
46 | -----
47 |
48 | >>> c = DiskCache()
49 | >>> c.refresh_token = token
50 | >>> c.access_token = token
51 | """
52 | def __init__(self, config_dir, filename):
53 | self.config_dir = os.path.expanduser(config_dir)
54 | if not os.path.isdir(self.config_dir):
55 | raise ConfigurationError("Directory %s not found. Configuration "
56 | "likely incomplete. "
57 | "Try pyTD.configure()" % self.config_dir)
58 | self.filename = filename
59 | self.config_path = os.path.join(self.config_dir, self.filename)
60 | self._create()
61 |
62 | def clear(self):
63 | """
64 | Empties the cache, though does not delete the cache file
65 | """
66 | with open(self.config_path, 'w') as f:
67 | json_data = {
68 | "refresh_token": None,
69 | "access_token": None,
70 | }
71 | f.write(json.dumps(json_data))
72 | f.close()
73 | return
74 |
75 | def _create(self):
76 | if not os.path.exists(self.config_path):
77 | with open(self.config_path, 'w') as f:
78 | json_data = {
79 | "refresh_token": None,
80 | "access_token": None,
81 | }
82 | f.write(json.dumps(json_data))
83 | f.close()
84 | return
85 |
86 | def _exists(self):
87 | """
88 | Utility function to test whether the configuration exists
89 | """
90 | return os.path.isfile(self.config_path)
91 |
92 | def _get(self, value=None):
93 | """
94 | Retrieves configuration information. If not passed a parameter,
95 | returns all configuration as a dictionary
96 |
97 | Parameters
98 | ----------
99 | value: str, optional
100 | Desired configuration value to retrieve
101 | """
102 | if self._exists() is True:
103 | f = open(self.config_path, 'r')
104 | config = json.load(f)
105 | if value is None:
106 | return config
107 | elif value not in config:
108 | raise ValueError("Value %s not found in configuration "
109 | "file." % value)
110 | else:
111 | if value == "refresh_token" and config[value]:
112 | return RefreshToken(config[value])
113 | elif value == "access_token" and config[value]:
114 | return AccessToken(config[value])
115 | else:
116 | return config[value]
117 | else:
118 | raise ConfigurationError("Configuration file not found in "
119 | "%s." % self.config_path)
120 |
121 | def _set(self, attr, payload):
122 | """
123 | Update configuration file given payload
124 |
125 | Parameters
126 | ----------
127 | payload: dict
128 | Dictionary of updated configuration variables
129 | """
130 | with open(self.config_path) as f:
131 | json_data = json.load(f)
132 | f.close()
133 | json_data.update({attr: payload.__dict__()})
134 | with open(self.config_path, "w") as f:
135 | f.write(json.dumps(json_data))
136 | f.close()
137 | return True
138 | raise ConfigurationError("Could not update config file")
139 |
--------------------------------------------------------------------------------
/docs/source/conf.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | sys.path.insert(0, os.path.abspath('..'))
4 | sys.path.insert(0, os.path.abspath('../..'))
5 | # -*- coding: utf-8 -*-
6 | #
7 | # Configuration file for the Sphinx documentation builder.
8 | #
9 | # This file does only contain a selection of the most common options. For a
10 | # full list see the documentation:
11 | # http://www.sphinx-doc.org/en/master/config
12 |
13 | # -- Path setup --------------------------------------------------------------
14 |
15 | # If extensions (or modules to document with autodoc) are in another directory,
16 | # add these directories to sys.path here. If the directory is relative to the
17 | # documentation root, use os.path.abspath to make it absolute, like shown here.
18 | #
19 | # import os
20 | # import sys
21 | # sys.path.insert(0, os.path.abspath('.'))
22 |
23 |
24 | # -- Project information -----------------------------------------------------
25 |
26 | project = 'pyTD'
27 | copyright = '2018, Addison Lynch'
28 | author = 'Addison Lynch'
29 |
30 | # The short X.Y version
31 | version = ''
32 | # The full version, including alpha/beta/rc tags
33 | release = ''
34 |
35 |
36 | # -- General configuration ---------------------------------------------------
37 |
38 | # If your documentation needs a minimal Sphinx version, state it here.
39 | #
40 | # needs_sphinx = '1.0'
41 |
42 | # Add any Sphinx extension module names here, as strings. They can be
43 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
44 | # ones.
45 | extensions = [
46 | "sphinx.ext.autodoc",
47 | "sphinx.ext.napoleon",
48 | "sphinx.ext.todo",
49 | "sphinx.ext.autosectionlabel",
50 | 'IPython.sphinxext.ipython_console_highlighting',
51 | 'IPython.sphinxext.ipython_directive',
52 | 'numpydoc'
53 | ]
54 |
55 | # Add any paths that contain templates here, relative to this directory.
56 | templates_path = ['_templates']
57 |
58 | # The suffix(es) of source filenames.
59 | # You can specify multiple suffix as a list of string:
60 | #
61 | # source_suffix = ['.rst', '.md']
62 | source_suffix = '.rst'
63 |
64 | # The master toctree document.
65 | master_doc = 'index'
66 |
67 | # The language for content autogenerated by Sphinx. Refer to documentation
68 | # for a list of supported languages.
69 | #
70 | # This is also used if you do content translation via gettext catalogs.
71 | # Usually you set "language" from the command line for these cases.
72 | language = None
73 |
74 | # List of patterns, relative to source directory, that match files and
75 | # directories to ignore when looking for source files.
76 | # This pattern also affects html_static_path and html_extra_path .
77 | exclude_patterns = []
78 |
79 | # The name of the Pygments (syntax highlighting) style to use.
80 | pygments_style = 'sphinx'
81 |
82 |
83 | # -- Options for HTML output -------------------------------------------------
84 |
85 | # The theme to use for HTML and HTML Help pages. See the documentation for
86 | # a list of builtin themes.
87 | #
88 | html_theme = "sphinx_rtd_theme"
89 | html_theme_path = ["_themes", ]
90 |
91 | # Theme options are theme-specific and customize the look and feel of a theme
92 | # further. For a list of options available for each theme, see the
93 | # documentation.
94 | #
95 | # html_theme_options = {}
96 |
97 | # Add any paths that contain custom static files (such as style sheets) here,
98 | # relative to this directory. They are copied after the builtin static files,
99 | # so a file named "default.css" will overwrite the builtin "default.css".
100 | html_static_path = ['_static']
101 |
102 | # Custom sidebar templates, must be a dictionary that maps document names
103 | # to template names.
104 | #
105 | # The default sidebars (for documents that don't match any pattern) are
106 | # defined by theme itself. Builtin themes are using these templates by
107 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
108 | # 'searchbox.html']``.
109 | #
110 | # html_sidebars = {}
111 |
112 |
113 | # -- Options for HTMLHelp output ---------------------------------------------
114 |
115 | # Output file base name for HTML help builder.
116 | htmlhelp_basename = 'pyTDdoc'
117 |
118 |
119 | # -- Options for LaTeX output ------------------------------------------------
120 |
121 | latex_elements = {
122 | # The paper size ('letterpaper' or 'a4paper').
123 | #
124 | # 'papersize': 'letterpaper',
125 |
126 | # The font size ('10pt', '11pt' or '12pt').
127 | #
128 | # 'pointsize': '10pt',
129 |
130 | # Additional stuff for the LaTeX preamble.
131 | #
132 | # 'preamble': '',
133 |
134 | # Latex figure (float) alignment
135 | #
136 | # 'figure_align': 'htbp',
137 | }
138 |
139 | # Grouping the document tree into LaTeX files. List of tuples
140 | # (source start file, target name, title,
141 | # author, documentclass [howto, manual, or own class]).
142 | latex_documents = [
143 | (master_doc, 'pyTD.tex', 'pyTD Documentation',
144 | 'Addison Lynch', 'manual'),
145 | ]
146 |
147 |
148 | # -- Options for manual page output ------------------------------------------
149 |
150 | # One entry per manual page. List of tuples
151 | # (source start file, name, description, authors, manual section).
152 | man_pages = [
153 | (master_doc, 'pytd', 'pyTD Documentation',
154 | [author], 1)
155 | ]
156 |
157 |
158 | # -- Options for Texinfo output ----------------------------------------------
159 |
160 | # Grouping the document tree into Texinfo files. List of tuples
161 | # (source start file, target name, title, author,
162 | # dir menu entry, description, category)
163 | texinfo_documents = [
164 | (master_doc, 'pyTD', 'pyTD Documentation',
165 | author, 'pyTD', 'One line description of project.',
166 | 'Miscellaneous'),
167 | ]
168 |
--------------------------------------------------------------------------------
/pyTD/auth/server.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import codecs
24 | import datetime
25 | import json
26 | import logging
27 | import os
28 | import requests
29 | import ssl
30 |
31 | from pyTD import BASE_AUTH_URL, DEFAULT_SSL_DIR, PACKAGE_DIR
32 | from pyTD.compat import HTTPServer, BaseHTTPRequestHandler
33 | from pyTD.compat import urlparse, urlencode, parse_qs
34 | from pyTD.utils import to_timestamp
35 | from pyTD.utils.exceptions import AuthorizationError
36 |
37 | logger = logging.getLogger(__name__)
38 |
39 |
40 | class Handler(BaseHTTPRequestHandler):
41 |
42 | STATIC_DIR = os.path.join(PACKAGE_DIR, '/auth/_static/')
43 |
44 | @property
45 | def auth_link(self):
46 | params = {
47 | "response_type": "code",
48 | "redirect_uri": self.server.callback_url,
49 | "client_id": self.server.consumer_key,
50 | }
51 | return '%s?%s' % (BASE_AUTH_URL, urlencode(params))
52 |
53 | def do_GET(self):
54 | if self.path.endswith(".css"):
55 | f = open("pyTD/auth/_static/style.css", 'r')
56 | self.send_response(200)
57 | self.send_header('Content-type', 'text/css')
58 | self.end_headers()
59 | self.wfile.write(f.read().encode())
60 | f.close()
61 | return
62 |
63 | self._set_headers()
64 | path, _, query_string = self.path.partition('?')
65 | try:
66 | code = parse_qs(query_string)['code'][0]
67 | except KeyError:
68 | f = codecs.open("pyTD/auth/_static/auth.html", "r", "utf-8")
69 | auth = f.read()
70 | link = auth.format(self.auth_link)
71 | self.wfile.write(link.encode('utf-8'))
72 | f.close()
73 | else:
74 | self.server.auth_code = code
75 | headers = {'Content-Type': 'application/x-www-form-urlencoded'}
76 | data = {'refresh_token': '', 'grant_type': 'authorization_code',
77 | 'access_type': 'offline', 'code': self.server.auth_code,
78 | 'client_id': self.server.consumer_key,
79 | 'redirect_uri': self.server.callback_url}
80 | now = to_timestamp(datetime.datetime.now())
81 | authReply = requests.post('https://api.tdameritrade.com/v1/oauth2/'
82 | 'token', headers=headers, data=data)
83 | try:
84 | json_data = authReply.json()
85 | json_data["access_time"] = now
86 | self.server._store_tokens(json_data)
87 | except ValueError:
88 | msg = json.dumps(json_data)
89 | logger.Error("Tokens could not be obtained")
90 | logger.Error("RESPONSE: %s" % msg)
91 | raise AuthorizationError("Authorization could not be "
92 | "completed")
93 | success = codecs.open("pyTD/auth/_static/success.html", "r",
94 | "utf-8")
95 |
96 | self.wfile.write(success.read().encode())
97 | success.close()
98 |
99 | def _set_headers(self):
100 | self.send_response(200)
101 | self.send_header('Content-Type', 'text/html')
102 | self.end_headers()
103 |
104 |
105 | class TDAuthServer(HTTPServer):
106 | """
107 | HTTP Server to handle authorization
108 | """
109 | def __init__(self, consumer_key, callback_url, retry_count=3):
110 | self.consumer_key = consumer_key
111 | self.callback_url = callback_url
112 | self.parsed_url = urlparse(self.callback_url)
113 | self.retry_count = retry_count
114 | self.auth_code = None
115 | self.tokens = None
116 | self.ssl_key = os.path.join(DEFAULT_SSL_DIR, 'key.pem')
117 | self.ssl_cert = os.path.join(DEFAULT_SSL_DIR, 'cert.pem')
118 | super(TDAuthServer, self).__init__(('localhost', self.port),
119 | Handler)
120 | self.socket = ssl.wrap_socket(self.socket, keyfile=self.ssl_key,
121 | certfile=self.ssl_cert,
122 | server_side=True)
123 |
124 | @property
125 | def hostname(self):
126 | return "%s://%s" % (self.parsed_url.scheme, self.parsed_url.hostname)
127 |
128 | @property
129 | def port(self):
130 | return self.parsed_url.port
131 |
132 | def _store_tokens(self, tokens):
133 | self.tokens = tokens
134 |
135 | def _wait_for_tokens(self):
136 | count = 0
137 | while count <= self.retry_count and self.tokens is None:
138 | self.handle_request()
139 | if self.tokens:
140 | return self.tokens
141 | else:
142 | raise AuthorizationError("The authorization could not be "
143 | "completed.")
144 |
--------------------------------------------------------------------------------
/pyTD/market/price_history.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import datetime
24 | import pandas as pd
25 |
26 | from pyTD.auth import auth_check
27 | from pyTD.market.base import MarketData
28 | from pyTD.utils import _sanitize_dates, to_timestamp, _handle_lists
29 | from pyTD.utils.exceptions import ResourceNotFound
30 |
31 |
32 | class PriceHistory(MarketData):
33 | """
34 | Class for retrieving data from the Get Price History endpoint. Defaults to
35 | a 10-day, 1-minute chart
36 |
37 | Parameters
38 | ----------
39 | symbols : string, array-like object (list, tuple, Series), or DataFrame
40 | Desired symbols for retrieval
41 | period_type: str, default "day", optional
42 | Type of period to show (valid values are day, month, year, or ytd)
43 | period: int, optional
44 | The number of periods to show
45 | frequency_type: str, optional
46 | The type of frequency with which a new candle is formed. (valid values
47 | are minute, daily, weekly, monthly, depending on period type)
48 | frequency: int, default 1, optional
49 | The number of the frequency type to be included in each candle
50 | startDate : string or DateTime object, optional
51 | Starting date, timestamp. Parses many different kind of date. Defaults
52 | to 1/1/2018
53 | representations (e.g., 'JAN-01-2015', '1/1/15', 'Jan, 1, 1980')
54 | endDate : string or DateTime object, optional
55 | Ending date, timestamp. Parses many different kind of date
56 | representations (e.g., 'JAN-01-2015', '1/1/15', 'Jan, 1, 1980').
57 | Defaults to current day
58 | extended: bool, default True, optional
59 | True to return extended hours data, False for regular market hours only
60 | output_format: str, optional, default 'pandas'
61 | Desired output format (json or Pandas DataFrame)
62 | api: pyTD.api.api object, optional
63 | A pyTD api object. If not passed, API requestor defaults to
64 | pyTD.api.default_api
65 | """
66 |
67 | def __init__(self, symbols, **kwargs):
68 | self.period_type = kwargs.pop("period_type", "month")
69 | self.period = kwargs.pop("period", "")
70 | self.frequency_type = kwargs.pop("frequency_type", "daily")
71 | self.frequency = kwargs.pop("frequency", "")
72 | start = kwargs.pop("start_date", datetime.datetime(2018, 1, 1))
73 | end = kwargs.pop("end_date", datetime.datetime.today())
74 | self.need_extended = kwargs.pop("extended", "")
75 | self.output_format = kwargs.pop("output_format", 'pandas')
76 | self.opt = kwargs
77 | api = kwargs.get("api")
78 | self.start, self.end = _sanitize_dates(start, end, set_defaults=False)
79 | if self.start and self.end:
80 | self.start = to_timestamp(self.start) * 1000
81 | self.end = to_timestamp(self.end) * 1000
82 | self.symbols = _handle_lists(symbols)
83 | super(PriceHistory, self).__init__(self.output_format, api)
84 |
85 | @property
86 | def params(self):
87 | p = {
88 | "periodType": self.period_type,
89 | "period": self.period,
90 | "frequencyType": self.frequency_type,
91 | "frequency": self.frequency,
92 | "startDate": self.start,
93 | "endDate": self.end,
94 | "needExtendedHoursData": self.need_extended
95 | }
96 | return p
97 |
98 | @property
99 | def resource(self):
100 | return 'pricehistory'
101 |
102 | @property
103 | def url(self):
104 | return "%s%s/{}/%s" % (self._BASE_URL, self.endpoint, self.resource)
105 |
106 | def _convert_output(self, out):
107 | for sym in self.symbols:
108 | out[sym] = self._convert_output_one(out[sym])
109 | return pd.concat(out.values(), keys=out.keys(), axis=1)
110 |
111 | def _convert_output_one(self, out):
112 | df = pd.DataFrame(out)
113 | df = df.set_index(pd.DatetimeIndex(df["datetime"]/1000*10**9))
114 | df = df.drop("datetime", axis=1)
115 | return df
116 |
117 | @auth_check
118 | def execute(self):
119 | result = {}
120 | for sym in self.symbols:
121 | data = self.get(url=self.url.format(sym))["candles"]
122 | FMT = "Price history for {} could not be retrieved"
123 | if not data:
124 | raise ResourceNotFound(message=FMT.format(sym))
125 | result[sym] = data
126 | if len(self.symbols) == 1:
127 | return self._output_format_one(result)
128 | else:
129 | return self._output_format(result)
130 |
131 | def _output_format_one(self, out):
132 | out = out[self.symbols[0]]
133 | if self.output_format == 'json':
134 | return out
135 | elif self.output_format == 'pandas':
136 | return self._convert_output_one(out)
137 | else:
138 | raise ValueError("Please enter a valid output format.")
139 |
--------------------------------------------------------------------------------
/pyTD/auth/manager.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import datetime
24 | import logging
25 | import requests
26 | import webbrowser
27 |
28 | from pyTD.auth.server import TDAuthServer
29 | from pyTD.auth.tokens import RefreshToken, AccessToken
30 | from pyTD.utils import to_timestamp
31 | from pyTD.utils.exceptions import AuthorizationError
32 |
33 | logger = logging.getLogger(__name__)
34 |
35 |
36 | class TDAuthManager(object):
37 | """
38 | Authorization manager for TD Ameritrade OAuth 2.0 authorization and
39 | authentication.
40 |
41 | Attributes
42 | ----------
43 | auth_server: TDAuthServer or None
44 | An authentication server instance which can be started and stopped for
45 | handling authentication redirects.
46 | """
47 | def __init__(self, token_cache, consumer_key, callback_url):
48 | """
49 | Initialize the class
50 |
51 | Parameters
52 | ----------
53 | token_cache: MemCache or DiskCache
54 | A cache for storing the refresh and access tokens
55 | consumer_key: str
56 | Client OAuth ID
57 | callback_url: str
58 | Client Redirect URI
59 | """
60 | self.cache = token_cache
61 | self.consumer_key = consumer_key
62 | self.callback_url = callback_url
63 | self.auth_server = None
64 |
65 | @property
66 | def access_token(self):
67 | return self.cache.access_token
68 |
69 | @property
70 | def refresh_token(self):
71 | return self.cache.refresh_token
72 |
73 | def auth_via_browser(self):
74 | """
75 | Handles authentication and authorization.
76 |
77 | Raises
78 | ------
79 | AuthorizationError
80 | If the authentication or authorization could not be completed
81 | """
82 | self._start_auth_server()
83 | self._open_browser(self.callback_url)
84 | logger.debug("Waiting for authorization code...")
85 | tokens = self.auth_server._wait_for_tokens()
86 | self._stop_auth_server()
87 |
88 | try:
89 | refresh_token = tokens["refresh_token"]
90 | refresh_expiry = tokens["refresh_token_expires_in"]
91 | access_token = tokens["access_token"]
92 | access_expiry = tokens["expires_in"]
93 | access_time = tokens["access_time"]
94 | except KeyError:
95 | logger.error("Authorization could not be completed.")
96 | raise AuthorizationError("Authorization could not be completed.")
97 | r = RefreshToken(token=refresh_token, access_time=access_time,
98 | expires_in=refresh_expiry)
99 | a = AccessToken(token=access_token, access_time=access_time,
100 | expires_in=access_expiry)
101 | logger.debug("Refresh and Access tokens received.")
102 | return (r, a,)
103 |
104 | def _open_browser(self, url):
105 | logger.info("Opening browser to %s" % url)
106 | webbrowser.open(url, new=2)
107 | return True
108 |
109 | def refresh_access_token(self):
110 | """
111 | Attempts to refresh access token if current is not valid.
112 |
113 | Updates the cache if new token is received.
114 |
115 | Raises
116 | ------
117 | AuthorizationError
118 | If the access token is not successfully refreshed
119 | """
120 | if self.cache.refresh_token.valid is False:
121 | raise AuthorizationError("Refresh token is not valid.")
122 | logger.debug("Attempting to refresh access token...")
123 | headers = {'Content-Type': 'application/x-www-form-urlencoded'}
124 | data = {'grant_type': 'refresh_token',
125 | 'refresh_token': self.cache.refresh_token.token,
126 | 'client_id': self.consumer_key}
127 | try:
128 | authReply = requests.post('https://api.tdameritrade.com/v1/oauth2/'
129 | 'token', headers=headers, data=data)
130 | now = to_timestamp(datetime.datetime.now())
131 | if authReply.status_code == 400:
132 | raise AuthorizationError("Could not refresh access token.")
133 | authReply.raise_for_status()
134 | json_data = authReply.json()
135 | token = json_data["access_token"]
136 | expires_in = json_data["expires_in"]
137 | except (KeyError, ValueError):
138 | logger.error("Error retrieving access token.")
139 | raise AuthorizationError("Error retrieving access token.")
140 | access_token = AccessToken(token=token, access_time=now,
141 | expires_in=expires_in)
142 | logger.debug("Successfully refreshed access token.")
143 | self.cache.access_token = access_token
144 |
145 | def _start_auth_server(self):
146 | logger.info("Starting authorization server")
147 |
148 | # Return if server is already running
149 | if self.auth_server is not None:
150 | return
151 | self.auth_server = TDAuthServer(self.consumer_key, self.callback_url)
152 |
153 | def _stop_auth_server(self):
154 | logger.info("Shutting down authorization server")
155 | if self.auth_server is None:
156 | return
157 | self.auth_server = None
158 |
--------------------------------------------------------------------------------
/pyTD/utils/testing.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | from pyTD.api import default_api
24 | from pyTD.compat import HTTPError
25 | from pyTD.utils.exceptions import ConfigurationError
26 |
27 |
28 | def default_auth_ok():
29 | """
30 | Used for testing. Returns true if a default API object is authorized
31 | """
32 | global __api__
33 | if __api__ is None:
34 | try:
35 | a = default_api()
36 | return a.auth_valid
37 | except ConfigurationError:
38 | return False
39 | else:
40 | if __api__.refresh_valid is True:
41 | return True
42 | else:
43 | return False
44 |
45 |
46 | class MockResponse(object):
47 | """
48 | Class for mocking HTTP response objects
49 | """
50 |
51 | def __init__(self, text, status_code, request_url=None,
52 | request_params=None, request_headers=None):
53 | """
54 | Initialize the class
55 |
56 | Parameters
57 | ----------
58 | text: str
59 | A plaintext string of the response
60 | status_code: int
61 | HTTP response code
62 | url: str, optional
63 | Request URL
64 | request_params: dict, optional
65 | Request Parameters
66 | request_headers: dict, optional
67 | Request headers
68 | """
69 | self.text = text
70 | self.status_code = status_code
71 | self.url = request_url
72 | self.request_params = request_params
73 | self.request_headers = request_headers
74 |
75 | def json(self):
76 | import json
77 | return json.loads(self.text)
78 |
79 | def raise_for_status(self):
80 | # Pulled directly from requests source code
81 | reason = ''
82 | http_error_msg = ''
83 | if 400 <= self.status_code < 500:
84 | http_error_msg = u'%s Client Error: %s for url: %s' % (
85 | self.status_code, reason, self.url)
86 |
87 | elif 500 <= self.status_code < 600:
88 | http_error_msg = u'%s Server Error: %s for url: %s' % (
89 | self.status_code, reason, self.url)
90 |
91 | if http_error_msg:
92 | raise HTTPError(http_error_msg, response=self)
93 |
94 |
95 | MOCK_SSL_CERT = """\
96 | -----BEGIN CERTIFICATE-----
97 | MIIDtTCCAp2gAwIBAgIJAPuEP7NccyjCMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
98 | BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
99 | aWRnaXRzIFB0eSBMdGQwHhcNMTgwNzAzMDIzODMwWhcNMTkwNzAzMDIzODMwWjBF
100 | MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
101 | ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
102 | CgKCAQEA/S/ocvpHNqQvuVtKqZi4JJbWRmw0hG2rS8NwXsn7YBkvPydvc9+CX5ZC
103 | Tdt93Hh2g6t07+EDjFQdWzuD1paKoLsjI3RTGM9OhY25AF13jsgdCORSetKiAuQy
104 | zKWtzLJ7egfjj8ZQdaUKhRONqLYu8IbtcQFuuL+B49xwPIfafMCmy6US/R6maCTH
105 | zeIw8LahV4ECM9NttfIJTkEkN/O8D30rJVZbpMhJHq+Y4rh94oBVW4JJMc+VZlHi
106 | C9d6E9yIiUtcKSsOZkZ3FL0TNEm2dmzI69wufC53B6NynYFVA0yhtvRgOZYdoFX6
107 | cMhk3Ciy7nFav+fdZ4PsJirATjtisQIDAQABo4GnMIGkMB0GA1UdDgQWBBRtfob1
108 | mHz0mr5YHvSYQ728X4Sz7zB1BgNVHSMEbjBsgBRtfob1mHz0mr5YHvSYQ728X4Sz
109 | 76FJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNV
110 | BAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAPuEP7NccyjCMAwGA1UdEwQF
111 | MAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAMUq5ZcfIzJF4nk3HqHxyajJJZNUarTU
112 | aizCqDcLSU+SgcrsrVu51s5OGpK+HhwwkY5uq5C1yv0tYc7e0V9e/dpANvUR5RMv
113 | Tme60HfJKioqhzSaNSz87a3TZayYhnREVfA6UqVL6EQ2ArVeqnn+mmrZ/oU5TJ9T
114 | Opwr8Kah78xnC/0iOWOR4IXliakNHdO0qqJIYlbpBxM7znYT6vPbvp/IQC7PA8qP
115 | AMce1keJ5u462aCza6zp95sFhqneDlI9lh9EA31eUfPgvdNPfqQP40DCQGSnvdeU
116 | fPm9pF9V4FSlznPyRJI4AgZOqpt580+GWTtYQBwPCqZSHq66f83Lmz4=
117 | -----END CERTIFICATE-----
118 | """
119 |
120 |
121 | MOCK_SSL_KEY = """\
122 | -----BEGIN RSA PRIVATE KEY-----
123 | MIIEowIBAAKCAQEA/S/ocvpHNqQvuVtKqZi4JJbWRmw0hG2rS8NwXsn7YBkvPydv
124 | c9+CX5ZCTdt93Hh2g6t07+EDjFQdWzuD1paKoLsjI3RTGM9OhY25AF13jsgdCORS
125 | etKiAuQyzKWtzLJ7egfjj8ZQdaUKhRONqLYu8IbtcQFuuL+B49xwPIfafMCmy6US
126 | /R6maCTHzeIw8LahV4ECM9NttfIJTkEkN/O8D30rJVZbpMhJHq+Y4rh94oBVW4JJ
127 | Mc+VZlHiC9d6E9yIiUtcKSsOZkZ3FL0TNEm2dmzI69wufC53B6NynYFVA0yhtvRg
128 | OZYdoFX6cMhk3Ciy7nFav+fdZ4PsJirATjtisQIDAQABAoIBAQChwFKj6gtG+FvY
129 | 8l7fvMaf8ZGRSh2/IQVXkNOgay/idBSAJ2SHxZpYEPnpHbnp+TfV5Nr/SWTn6PEc
130 | UQhoNqL4DrZjNzTDW+XRYvp3Jj90g5oxDRU4jIqeiEWAArTnWnuSOaoDN3I9xqPS
131 | 4uwUhde1KK5XDNA8zXRhK3q04SIPogtgyzYY9D+6TVF/F+34jhFG6TDjnuIP9PwG
132 | l6eY+b7q1zspcqAXFXVJ5xxhkI79zmH0SoVKEz7VAtqdDi3dKfsInexjiLET4ibV
133 | YcBgW0PRA0ZDw10EOjDAOZBzr1jitUuQ3VJI8XaWQaWt33tD7iVEdwDJt88w0YIc
134 | bgtlIIXlAoGBAP63Fl8OWhgtcjVg/+idQg08HM/Tv6Ri/jtpWTnPmQolW8bCqB7M
135 | SIc0DkHKluqTzTkNFD890WgKGTjV5UFXMFREtrRQJuycIfHg0FvGCtpYVjtqqgjn
136 | 0IVHfGVJ5Q3mFeSqMj8cheb5Nk767P66gd2gTLTFgca0Wh1Vf1ykBY3DAoGBAP52
137 | 2PMXrTBKsssXGPmA4/0HVvd1f0JzEH4ithhYwSvkNfwv+EdW8hriNVj5LL4sMC4j
138 | P2hZKC7c39paG4MVvBHQ9AhgrH97VXxFzjIECTx9VINyR3yxT5Nqn6ilmTR1gmty
139 | gdlEztFVloUlGrfHh8cGTGI6J7eYFCnk7NzGrEJ7AoGANu7ZfkqkF47FkMmIp2wy
140 | 8JPESvYJ4LQQzFNeEN+6y7te3bDhfTLleXM6l+nPPmv92I3/jdwRK3TyF5XZyYu6
141 | OpJPLPgUTPcnQvkPNpuxf4GJp2rLnPwRtozCQT38jlDO6+/gwkeugS/CDKqFLjKf
142 | C2Mk59+oq2f9/1GPFDWzlO0CgYAW8XZMLMVTxlhqkVGSJXno9YF03GY2ApPpG44Z
143 | kd8Q6wmnDFgxbnhzzhOLSyQqnWdWsZzk9qz11LpmQJucbRhA7vshyj2jXOZvRwf5
144 | YH3Is3AsTeB+MKqBGyr8FLpEjZfNwkxM37RaEYJ5zMek7FukqT+315B/MDoZMOfe
145 | XBdqAwKBgD1CoyKb7Cgcb8zEHMkVAPP7tljpO1/gzuXRSOp7G4blKK+fF40vSh79
146 | azBtciC6VbBwUPRW4OY9qPqhOMA3DAgeJZBrCrEkQVHWqW2u0FOdJsMDz5TpDQSV
147 | cHy9ZQCz9WDroSC21Z0BFJ8DKPXvFL/XjlCtpfBP7JFoAChm5MeW
148 | -----END RSA PRIVATE KEY-----
149 | """
150 |
--------------------------------------------------------------------------------
/docs/source/quickstart.rst:
--------------------------------------------------------------------------------
1 | .. _quickstart:
2 |
3 | Quick Start Guide
4 | =================
5 |
6 | .. note:: This Quick Start guide assumes the use of a :ref:`Script
7 | Application `. See :ref:`Authentication` for
8 | more information about using
9 | **installed** applications and **web** applications.
10 |
11 | In this section, we go over everything you need to know to start building
12 | scripts, or bots using pyTD, the Python TD Ameritrade Developer API SDK.
13 | It's fun and easy. Let's get started.
14 |
15 | Prerequisites
16 | -------------
17 |
18 | :Python Knowledge: You need to know at least a little Python to use pyTD; it's
19 | a Python wrapper after all. pyTD supports `Python 2.7`_,
20 | and `Python 3.4 to 3.7`_.
21 |
22 | :TD Ameritrade Knowledge: A basic understanding of how TD Ameritrade's
23 | Developer APIs work is a must. It is recommended that you read
24 | through the `TD Ameritrade documentation`_ before starting
25 | with pyTD.
26 |
27 | :TD Ameritrade Developer Account: This is a **separate account** from your TD
28 | brokerage accounts(s).
29 |
30 | .. _`Python 2.7`: https://docs.python.org/2/tutorial/index.html
31 | .. _`Python 3.4 to 3.7`: https://docs.python.org/3/tutorial/index.html
32 | .. _`TD Ameritrade documentation`: https://developer.tdameritrade.com/apis
33 |
34 | .. _quickstart.common_tasks:
35 |
36 |
37 | Step 1 - Obtain an Consumer Key and Callback URL
38 | ------------------------------------------------
39 |
40 | .. seealso:: For a more detailed tutorial on setting up a TD Ameritrade
41 | Developer Account, creating an application, or obtaining an Consumer Key and
42 | Callback URL, see :ref:`How do I get my Consumer Key and Callback URL?`.
43 |
44 | 1. From your TD Ameritrade Developer Account, **create a new application**
45 | using the "Add App" button in your profile. Enter the following information
46 | when prompted:
47 |
48 | * **App Name** - desired application name (can be anything)
49 | * **Callback URL** - the address that your authentication information will be forwarded to complete authentication of your script application (https://localhost:8080
50 | is easiest). See :ref:`What should my Callback URL be?` for more information
51 | on choosing a Callback URL.
52 | * **OAuth User ID** - unique ID that will be used to create your consumer key
53 | (can be anything)
54 | * **App Description** - description of your application (can be anything)
55 |
56 | 2. Once your application has been created, store its **Consumer Key** and **Callback URL**
57 | in the environment variables ``TD_CONSUMER_KEY`` and ``TD_CALLBACK_URL``:
58 |
59 | .. code-block:: bash
60 |
61 | $ export TD_CONSUMER_KEY=TEST@AMER.OAUTHAP # Your Consumer Key
62 | $ export TD_CALLBACK_URL=https://localhost:8080 # Your Callback URL
63 |
64 | .. note:: If you are unfamiliar with environment variables or unable to
65 | set them on your system, see :ref:`Configuring Environment` for more
66 | configuration options.
67 |
68 |
69 |
70 | Step 2 - Run ``pyTD.configure``
71 | -------------------------------
72 |
73 |
74 | The easiest (and recommended) way configure ``pyTD`` is using
75 | ``pyTD.configure``.
76 |
77 |
78 | .. code-block:: python
79 |
80 | import pyTD
81 | pyTD.configure()
82 |
83 | This function does the following:
84 |
85 | 1. Creates a **configuration directory** (defaults to ``.tdm`` in your home
86 | directory). The location can be chosen manually by setting the environment
87 | variable ``TD_CONFIG_DIR``. This directory is the location in which pyTD's
88 | :ref:`log`, :ref:`cached tokens ` (if using on-disk
89 | caching), and :ref:`SSL certificate and key` are stored.
90 |
91 | 2. Generates a **self-signed SSL certificate \& key** and places them in the
92 | ``ssl`` directory within your configuration directory.
93 |
94 | .. warning:: If using MacOS, you may not be able to generate the certificate
95 | and key using ``pyTD.configure``. See :ref:`Generating an SSL Certificate and Key ` for
96 | more information and instructions on how to generate the
97 | certificate manually.
98 |
99 | .. note::
100 | When called with no arguments, ``pyTD.configure`` requires :ref:`setting environment
101 | variables` ``TD_CONSUMER_KEY`` and ``TD_CALLBACK_URL`` to your app's Consumer Key and
102 | Callback URL. These can also be passed to ``pyTD.configure`` instead:
103 |
104 | .. code:: python
105 |
106 | import pyTD
107 |
108 | consumer_key='TEST@AMER.OAUTHAP'
109 | callback_url='https://localhost:8080'
110 |
111 | pyTD.configure(consumer_key=consumer_key, callback_url=callback_url)
112 |
113 | ``pyTD.configure`` will set the environment variables automatically for the
114 | **current session only**.
115 |
116 | .. _`documentation`: https://addisonlynch.github.io/pytd/stable/faq.html#what-is-a-td-ameritrade-developer-account
117 | .. _`Generating an SSL Key/Certificate`: https://addisonlynch.github.io/pytd/stable/configuration.html#generating-an-ssl-key-certificate
118 | .. _`docs`: https://addisonlynch.github.io/pytd/stable/configuration.html#the-all-in-one-solution-pytd-configure
119 | .. _`configuration directory`: https://addisonlynch.github.io/pytd/stable/configuration.html#configuration-directory
120 |
121 | .. _quickstart.authenticate-app:
122 |
123 | Step 3 - Authenticate Your Application
124 | --------------------------------------
125 |
126 |
127 | The simplest way to authorize and authenticate pyTD is by calling any function which
128 | returns data. For example ``get_quotes`` from ``pyTD.market``
129 | will automatically prompt you to obtain a new refresh token if you have not
130 | obtained one or your refresh token has expired:
131 |
132 |
133 | .. code-block:: python
134 |
135 | from pyTD.market import get_quotes
136 |
137 | get_quotes("AAPL")
138 | # WARNING:root:Need new refresh token.
139 | # Would you like to authorize a new refresh token? [y/n]:
140 |
141 | Selecting ``y`` will open a browser for you to authorize your application:
142 |
143 | .. figure:: _static/img/authprompt.png
144 |
145 | Select "AUTHORIZE" to redirect to a TD Ameritrade login prompt:
146 |
147 | .. figure:: _static/img/tdlogin.png
148 |
149 | From here, log in to your TD Ameritrade Brokerage Account. Once logged in, the following page will be displayed:
150 |
151 | .. figure:: _static/img/tdallow.png
152 |
153 | Select "Allow" to authorize your application. pyTD will handle receiving the tokens and authorization code behind the scenes, and if retrieval is successful, the results of your original query will display on screen.
154 |
155 |
156 | Step 4 - Go!
157 | ------------
158 |
159 | You're now all set up to query TD Ameritrade's Developer APIs!
160 |
161 | .. seealso:: For more usage tutorials and examples, see :ref:`Tutorials
162 | `
163 |
--------------------------------------------------------------------------------
/pyTD/market/options.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | from pyTD.market.base import MarketData
24 | from pyTD.utils import _handle_lists
25 | from pyTD.utils.exceptions import ResourceNotFound
26 |
27 |
28 | class Options(MarketData):
29 | """
30 | Class for retrieving data from the Get Option Chain endpoint
31 |
32 | Parameters
33 | ----------
34 | symbol: str
35 | Desired ticker for retrieval
36 | contract_type: str, default "ALL", optional
37 | Type of contracts to return in the chain. Can be CALL,
38 | PUT, or ALL
39 | strike_count: int, optional
40 | The number of strikes to return above and below the
41 | at-the-money price
42 | include_quotes: bool, optional
43 | Include quotes for options in the option chain
44 | strategy: str, default "SINGLE", optional
45 | Passing a value returns a Strategy Chain. Possible values are SINGLE,
46 | ANALYTICAL, COVERED, VERTICAL, CALENDAR, STRANGLE, STRADDLE,
47 | BUTTERFLY, CONDOR, DIAGONAL, COLLAR, ROLL
48 | interval: int, optional
49 | Strike interval for spread strategy chains
50 | strike: float, optional
51 | Strike price to return options only at that strike price
52 | range: str, default "ALL", optional
53 | Returns options for the given range. Possible values are ITM, NTM,
54 | OTM, SAK, SBK, SNK, ALL
55 | from_date : datetime.datetime object, optional
56 | Only return expirations after this date
57 | to_date: datetime.datetime object, optional
58 | Only return expirations before this date
59 | volatility: int or float, optional
60 | Volatility to use in calculations
61 | underlying_price: int or float, optional
62 | Underlying price to use in calculations
63 | interest_rate: int or float, optional
64 | Interest rate to use in calculations
65 | days_to_expiration: int, optional
66 | Days to expiration to use in calculations
67 | exp_month: str, default "ALL", optional
68 | Return only options expiring in the specified month. Month is given in
69 | 3-character format (JAN, FEB, MAR, etc.)
70 | option_type: str, default "ALL", optional
71 | Type of contracts to return (S, NS, ALL)
72 | output_format: str, optional, default 'json'
73 | Desired output format
74 | api: pyTD.api.api object, optional
75 | A pyTD api object. If not passed, API requestor defaults to
76 | pyTD.api.default_api
77 | """
78 |
79 | def __init__(self, symbol, **kwargs):
80 | self.contract_type = kwargs.pop("contract_type", "ALL")
81 | self.strike_count = kwargs.pop("strike_count", "")
82 | self.include_quotes = kwargs.pop("include_quotes", "")
83 | self.strategy = kwargs.pop("strategy", "")
84 | self.interval = kwargs.pop("interval", "")
85 | self.strike = kwargs.pop("strike", "")
86 | self.range = kwargs.pop("range", "")
87 | self.from_date = kwargs.pop("from_date", "")
88 | self.to_date = kwargs.pop("to_date", "")
89 | self.volatility = kwargs.pop("volatility", "")
90 | self.underlying_price = kwargs.pop("underlying_price", "")
91 | self.interest_rate = kwargs.pop("interest_rate", "")
92 | self.days_to_expiration = kwargs.pop("days_to_expiration", "")
93 | self.exp_month = kwargs.pop("exp_month", "")
94 | self.option_type = kwargs.pop("option_type", "")
95 | self.output_format = kwargs.pop("output_format", 'pandas')
96 | self.api = kwargs.pop("api", None)
97 | self.opts = kwargs
98 | self.symbols = _handle_lists(symbol)
99 | super(Options, self).__init__(self.output_format, self.api)
100 |
101 | @property
102 | def params(self):
103 | p = {
104 | "symbol": self.symbols,
105 | "contractType": self.contract_type,
106 | "strikeCount": self.strike_count,
107 | "includeQuotes": self.include_quotes,
108 | "strategy": self.strategy,
109 | "interval": self.interval,
110 | "strike": self.strike,
111 | "range": self.range,
112 | "fromDate": self.from_date,
113 | "toDate": self.to_date,
114 | "volatility": self.volatility,
115 | "underlyingPrice": self.underlying_price,
116 | "interestRate": self.interest_rate,
117 | "daysToExpiration": self.days_to_expiration,
118 | "expMonth": self.exp_month,
119 | "optionType": self.option_type
120 | }
121 | p.update(self.opts)
122 | return p
123 |
124 | @property
125 | def resource(self):
126 | return 'chains'
127 |
128 | def _convert_output(self, out):
129 | import pandas as pd
130 | ret = {}
131 | ret2 = {}
132 | if self.contract_type in ["CALL", "ALL"]:
133 | for date in out['callExpDateMap']:
134 | for strike in out['callExpDateMap'][date]:
135 | ret[date] = (out['callExpDateMap'][date][strike])[0]
136 | if self.contract_type in ["PUT", "ALL"]:
137 | for date in out['putExpDateMap']:
138 | for strike in out['putExpDateMap'][date]:
139 | ret2[date] = (out['putExpDateMap'][date][strike])[0]
140 | return pd.concat([pd.DataFrame(ret).T, pd.DataFrame(ret2).T], axis=1,
141 | keys=["calls", "puts"])
142 |
143 | def get(self):
144 | data = super(Options, self).get()
145 | if data["status"] == "FAILED":
146 | raise ResourceNotFound(message="Option chains for %s not "
147 | "found." % self.symbols)
148 | return data
149 |
--------------------------------------------------------------------------------
/docs/source/configuration.rst:
--------------------------------------------------------------------------------
1 | .. _config:
2 |
3 |
4 | Configuration
5 | =============
6 |
7 |
8 | - :ref:`config.environment`
9 | - :ref:`config.user_agent`
10 | - :ref:`config.logging`
11 | - :ref:`config.appendix`
12 |
13 |
14 | The following are the available configuration options for pyTD.
15 |
16 |
17 | **Environment**
18 |
19 | :Configuration Directory:
20 | Location in which pyTD's :ref:`log `, :ref:`cached
21 | tokens ` (if using on-disk caching), and :ref:`SSL certificate and
22 | key` are stored
23 |
24 | :SSL Certificate and Key:
25 | If using local web server authentication (script applications), a
26 | self-signed SSL certificate and key are needed.
27 |
28 | **User Agent** - ``api``
29 |
30 | :Consumer Key \& Callback URL:
31 | TD Ameritrade authorization credentials
32 |
33 | :Token Caching:
34 | Storage of authentication tokens. Can be cached *on-disk* or *in-memory*.
35 |
36 | :Request Parameters:
37 | Specify how requests should be be made. These include ``retry_count``, ``pause``, and ``session``.
38 |
39 | **Logging**
40 |
41 | :Log Level:
42 | Logging level of pyTD. The ``logging`` module handles pyTD's logging.
43 |
44 | .. _config.environment:
45 |
46 | Configuring Environment
47 | -----------------------
48 |
49 | .. _config.config_dir:
50 |
51 | Configuration Directory
52 | ~~~~~~~~~~~~~~~~~~~~~~~
53 |
54 | By default, pyTD creates the directory ``.tdm`` in your home directory, which
55 | serves as the default location for on-disk token caching, pyTD's log, and your SSL
56 | certificate and key.
57 |
58 | To specify a custom configuration directory, store such directory's *absolute*
59 | path in the environment variable ``TD_CONFIG_DIR``:
60 |
61 | .. code:: bash
62 |
63 | $ export TD_CONFIG_DIR=
64 |
65 | replacing the bracketed text with your absolute path.
66 |
67 | .. _config.ssl:
68 |
69 | SSL Certificate and Key
70 | ~~~~~~~~~~~~~~~~~~~~~~~
71 |
72 | .. seealso:: :ref:`What is a self-signed SSL Certificate?`
73 |
74 | .. _config.ssl_auto:
75 |
76 | Automatic
77 | ^^^^^^^^^
78 |
79 | To generate the self-signed SSL key and certificate needed for local web server
80 | authentication, use the top-level ``configure`` function:
81 |
82 | .. code:: python
83 |
84 | from pyTD import configure
85 |
86 | configure()
87 |
88 | This function will prompt creation of a self-signed SSL key and certificate,
89 | which will both be placed in your :ref:`Configuration Directory`.
90 |
91 | .. _config.ssl_manual:
92 |
93 | Manual
94 | ^^^^^^
95 |
96 | The SSL key and certificate can be created manually with the
97 | following OpenSSL command:
98 |
99 | .. code:: bash
100 |
101 | $ openssl req -newkey rsa:2048 -nodes -keyout key.pem -x509 -days 365 -out cert.pem
102 |
103 | Place the generated key and certificate in the ``ssl`` sub-directory of your
104 | :ref:`config.config_dir`.
105 |
106 | .. _config.environment-variables:
107 |
108 | Setting Environment Variables
109 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
110 |
111 | MacOS/Linux
112 | ^^^^^^^^^^^
113 |
114 | To set the environment variables for your Consumer Key and Callback URL, use the
115 | following command:
116 |
117 | .. code:: bash
118 |
119 | $ export TD_CONSUMER_KEY=''
120 | $ export TD_CALLBACK_URL=''
121 |
122 | replacing the bracketed text with your information.
123 |
124 | .. _config.all_in_one:
125 |
126 |
127 | The All-in-One Solution: ``pyTD.configure``
128 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
129 |
130 | pyTD provides the top-level function ``pyTD.configure`` which handles all
131 | configuration necessary to make an authenticated query.
132 |
133 | .. autofunction:: pyTD.configure
134 |
135 |
136 |
137 | .. _config.user_agent:
138 |
139 | Configuring User Agent
140 | ----------------------
141 |
142 | .. _config.default-api:
143 |
144 | There are three ways to configure your user agent:
145 |
146 | 1. **Let pyTD do it for you**. So long as you have your :ref:`Configuration
147 | Directory` set up, simply run any pyTD function or method and pyTD will create
148 | an ``api`` object for you, defaulting to an on-disk token cache.
149 |
150 | .. code:: python
151 |
152 | from pyTD.market import get_quotes
153 |
154 | get_quotes("AAPL")
155 |
156 | 2. **Create a non-global API object**
157 |
158 | Either pass the required parameters individually:
159 |
160 | .. code:: python
161 |
162 | from pyTD.api import api
163 | from pyTD.market import get_quotes
164 |
165 | consumer_key = "TEST@AMER.OAUTHAP"
166 | callback_url = "https://localhost:8080"
167 |
168 | my_api = api(consumer_key=consumer_key, callback_url=callback_url)
169 |
170 | get_quotes("AAPL", api=my_api)
171 |
172 | Or pass a pre-instantiated dictonary:
173 |
174 | .. code:: python
175 |
176 | from pyTD.api import api
177 | from pyTD.market import get_quotes
178 |
179 | params = {
180 | "consumer_key": "TEST@AMER.OAUTHAP",
181 | "callback_url": "https://localhost:8080"
182 | }
183 |
184 | my_api = api(params)
185 |
186 | get_quotes("AAPL", api=my_api)
187 |
188 | 3. **Use** ``pyTD.configure``:
189 |
190 | .. code:: python
191 |
192 | from pyTD import configure
193 | from pyTD.market import get_quotes
194 |
195 | consumer_key = "TEST@AMER.OAUTHAP"
196 | callback_url = "https://localhost:8080"
197 |
198 | configure(consumer_key=consumer_key, callback_url=callback_url)
199 |
200 | get_quotes("AAPL")
201 |
202 | The ``api`` object
203 | ~~~~~~~~~~~~~~~~~~
204 |
205 | The ``api`` object serves as the user agent for all requests to the TD
206 | Ameritrade Developer API. The ``api`` object:
207 |
208 | 1. Manages configuration (directory, SSL, Consumer Key, Callback URL)
209 | 2. Connects to the token cache
210 | 3. Verifies, validates, and handles authentication and authorization.
211 |
212 |
213 | .. autoclass:: pyTD.api.api
214 |
215 |
216 |
217 | .. _config.logging:
218 |
219 | Configuring Logging
220 | -------------------
221 |
222 | pyTD uses Python's `logging
223 | `__ module to log its activity
224 | for both informational and debugging purposes.
225 |
226 | By default, a log is kept in pyTD's :ref:`Configuration Directory` and named
227 | ``pyTD.log``.
228 |
229 | Setting the logging level
230 | ~~~~~~~~~~~~~~~~~~~~~~~~~
231 |
232 | The console logging level of pyTD can be set in one of three ways:
233 |
234 | 1. **Using** ``pyTD.log_level``:
235 |
236 | .. code:: python
237 |
238 | import pyTD
239 |
240 | pyTD.log_level = "DEBUG"
241 |
242 | 2. **Using** ``logging.setLevel``:
243 |
244 | .. code:: python
245 |
246 | import logging
247 |
248 | logging.getLogger("pyTD").setLevel(logging.DEBUG)
249 |
250 | 3. **Using environment variables**
251 |
252 | The environment variable ``TD_LOG_LEVEL`` will override any log level settings for the console logger.
253 |
254 | .. code:: bash
255 |
256 | export TD_LOG_LEVEL='DEBUG'
257 |
258 | .. _config.appendix:
259 |
260 | Appendix
261 | --------
262 |
263 | Environment Variables
264 | ~~~~~~~~~~~~~~~~~~~~~
265 |
266 | For reference, the following environment variables may be set to configure pyTD:
267 |
268 | - ``TD_CONSUMER_KEY`` (required) - TD Ameritrade Developer application OAUTH ID
269 | - ``TD_CALLBACK_URL`` (required) - TD Ameritrade Developer application Callback URL
270 | - ``TD_CONFIG_DIR`` - for specifying a custom pyTD configuration directory (defaults to ~/.tdm)
271 | - ``TD_STORE_TOKENS`` - set to false to disable on-disk authentication token
272 | caching
273 | - ``TD_LOG_LEVEL`` - for specifying a console logging level for pyTD
274 |
--------------------------------------------------------------------------------
/docs/source/faq.rst:
--------------------------------------------------------------------------------
1 | .. _faq:
2 |
3 | Frequently Asked Questions
4 | ==========================
5 |
6 | .. _faq.oauth_20:
7 |
8 | What is OAuth 2.0?
9 | ------------------
10 |
11 | From `RFC 6749 `__:
12 |
13 |
14 |
15 | The OAuth 2.0 authorization framework enables a third-party
16 | application to obtain limited access to an HTTP service, either on
17 | behalf of a resource owner by orchestrating an approval interaction
18 | between the resource owner and the HTTP service, or by allowing the
19 | third-party application to obtain access on its own behalf.
20 |
21 | In other words, OAuth 2.0 is the protocol that TD Ameritrade uses to help you
22 | gain access to the Developer API (and your account). There are four roles in
23 | this process:
24 |
25 | 1. **Resource Owner** - an entity capable of granting access to a protected
26 | resource. **In this case, YOU**.
27 |
28 | 2. **Resource Server** - the server hosting the protected resources, capable of
29 | accepting and responding to protected resource requests using access tokens.
30 | **In this case, TD Ameritrade's servers**.
31 |
32 | 3. **Client** - An application making protected resource requests on behalf of
33 | the resource owner and with its authorization. **In this case, pyTD (or an
34 | application which uses it)**.
35 |
36 | 4. **Authorization Server** - The server issung access tokens to the client
37 | after successfully authenticating the resource owner and obtaining
38 | authorization. **In this case, your application or local authorization
39 | server**.
40 |
41 |
42 |
43 |
44 | .. _faq.callback-url:
45 |
46 | What should my Callback URL be?
47 | -------------------------------
48 |
49 | This raises an important question: **What is a Callback URL?**
50 |
51 | From `RFC 6749 `__:
52 |
53 | The authorization code is obtained by using an **authorization server**
54 | as an intermediary between the client and resource owner. Instead of
55 | requesting authorization directly from the resource owner, **the client
56 | directs the resource owner to an authorization server** (via its
57 | user-agent as defined in [RFC2616]), which in turn directs the
58 | resource owner back to the client with the authorization code....
59 |
60 | ... **Because the resource owner only authenticates with the authorization
61 | server, the resource owner's credentials are never shared with the client**.
62 |
63 | As explained in :ref:`What is OAuth 2.0?`, the resource owner is you - as you
64 | have the access to your TD Ameritrade brokerage account. In order to prevent
65 | your account credentials from being revealed, authentication is completed with
66 | an **authorization server**.
67 |
68 | Default ``pyTD`` behavior is to start this server locally, running on your
69 | localhost (typically ``127.0.0.1``). If this is the case, your Callback URL
70 | should be https://localhost:8080.
71 |
72 |
73 | .. _faq.ssl-basics:
74 |
75 | What is a self-signed SSL certificate?
76 | --------------------------------------
77 |
78 | An SSL certificate certifies the identity of an entity such as your local pyTD authentication server. **Self-signed SSL certificates** are signed by the same entity which they are certifying the identity of.
79 |
80 | This may cause problems for some browsers, which will display messages such as "Your connection is not private" and "This site's security certificate is not trusted!". This is due to the fact that your application is not a trusted certificate authority.
81 |
82 | .. _faq.create-ssl-cert-key:
83 |
84 | How do I create a self-signed SSL certificate and key?
85 | ------------------------------------------------------
86 |
87 | There are a number of different options for generating a self-signed SSL
88 | certificate and key.
89 |
90 | The easiest way: OpenSSL
91 | ~~~~~~~~~~~~~~~~~~~~~~~~
92 |
93 | `OpenSSL `__ is an SSL/TLS toolkit which is useful
94 | for generating SSL certificates. Once installed (see system-specific
95 | installation instructions below), run the following command to generate key
96 | and certificate files ``key.pem`` and ``cert.pem``:
97 |
98 | .. code-block:: bash
99 |
100 | openssl req -newkey rsa:2048 -nodes -keyout key.pem -x509 -days 365 -out cert.pem
101 |
102 |
103 | Installing OpenSSL
104 | ^^^^^^^^^^^^^^^^^^
105 |
106 | **macOS**
107 |
108 | Install using `Homebrew `__:
109 |
110 | .. code-block:: bash
111 |
112 | brew update
113 | brew install openssl
114 |
115 | **Linux**
116 |
117 | OpenSSL is packaged with most Linux distributions
118 |
119 |
120 | **Windows**
121 |
122 | OpenSSL for Windows can be downloaded `here `__.
123 |
124 |
125 | .. _faq.dev_account:
126 |
127 | How do I get my Consumer Key and Callback URL?
128 | ----------------------------------------------
129 |
130 | A TD Ameritrade Developer account and application are required in order to access the Developer API. **This is a separate account from TD Ameritrade Brokerage Accounts**. A TD Ameritrade Brokerage Account is **not required** to obtain a TD Ameritrade Developer Account.
131 |
132 | To register for a TD Ameritrade Developer account, visit https://developer.tdameritrade.com/ and click "Register" in the top-right corner of the screen.
133 |
134 | Creating an App
135 | ~~~~~~~~~~~~~~~
136 |
137 | To create a new TD Ameritrade Developer Application, navigate to the "My Apps" page of your TD Ameritrade Developer Account:
138 |
139 | .. figure:: _static/img/noapps.png
140 |
141 | My Apps Page
142 |
143 | From here, click the "Add a new App" button:
144 |
145 | .. figure:: _static/img/createapp.png
146 |
147 | Creating an app
148 |
149 | You will be prompted to enter the following fields:
150 |
151 | 1) **App Name** - desired application name
152 | 2) **Callback URL** - also known as Callback URL, this is the callback address to complete authorization
153 | 3) **OAuth User ID** - a unique ID that will be used to create your full OAauth ID
154 | 4) **App Description** - a description of your application
155 |
156 | After completing the form, your application will be created. By clicking on the application in the "My Apps" page, you can display information about it:
157 |
158 | .. figure:: _static/img/appinfo.png
159 |
160 | App Info
161 |
162 | The **Consumer Key** field is your **Consumer Key**. Your **Callback URL** is the **Callback URL** which you entered at the app's creation.
163 |
164 | .. note:: To change the Callback URL (Callback URL) of your application, you must delete the application and create a new one. This is a caveat of the TD Ameritrade registration process.
165 |
166 |
167 | .. _faq.script:
168 |
169 | What is a Script Application?
170 | -----------------------------
171 |
172 | A script application is simply an application that is run as a script from your
173 | local environment. This may be a stand-alone script that is run which uses pyTD
174 | or command-line invocation of pyTD, such as running:
175 |
176 | .. code-block:: python
177 |
178 | >>> from pyTD.market import get_quotes
179 | >>> get_quotes("AAPL")
180 |
181 | in an interactive Python shell.
182 |
183 |
184 | .. _faq.cusip:
185 |
186 | What is a CUSIP ID?
187 | -------------------
188 |
189 | A CUSIP is a nine-character alphanumeric code that identifies a North American
190 | financial security.
191 |
192 | Simply put, CUSIPs are unique identifiers for a number of financial instruments
193 | including common stocks, bonds, and other equities.
194 |
195 | .. _faq.cusip-examples:
196 |
197 | Examples
198 | ~~~~~~~~
199 |
200 | - Apple Inc.: 03783100
201 | - Cisco Systems: 17275R102
202 | - Google Inc.: 38259P508
203 | - Microsoft Corporation: 594918104
204 | - Oracle Corporation: 58389X105
205 |
206 | .. _faq.token_storage:
207 |
208 | Is it safe to save my authentications on-disk?
209 | ----------------------------------------------
210 |
211 | .. note:: TODO
212 |
--------------------------------------------------------------------------------
/pyTD/tests/unit/test_market.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import datetime
24 | import pandas as pd
25 | import pytest
26 |
27 | from pyTD.tests.test_helper import pyTD
28 |
29 | TDQueryError = pyTD.utils.exceptions.TDQueryError
30 | ResourceNotFound = pyTD.utils.exceptions.ResourceNotFound
31 |
32 |
33 | @pytest.fixture(scope='session', autouse=True)
34 | def now():
35 | return datetime.datetime.now()
36 |
37 |
38 | @pytest.mark.webtest
39 | class TestMarketExceptions(object):
40 |
41 | def test_get_quotes_bad_symbol(self):
42 | with pytest.raises(TypeError):
43 | pyTD.market.get_quotes()
44 |
45 | def test_get_movers_bad_index(self):
46 | with pytest.raises(TypeError):
47 | pyTD.market.get_movers()
48 |
49 | with pytest.raises(TDQueryError):
50 | pyTD.market.get_movers("DJI")
51 |
52 |
53 | @pytest.mark.webtest
54 | class TestQuotes(object):
55 |
56 | def test_quotes_json_single(self):
57 | aapl = pyTD.market.get_quotes("AAPL", output_format='json')
58 | assert isinstance(aapl, dict)
59 | assert "AAPL" in aapl
60 |
61 | def test_quotes_json_multiple(self):
62 | aapl = pyTD.market.get_quotes(["AAPL", "TSLA"],
63 | output_format='json')
64 | assert isinstance(aapl, dict)
65 | assert len(aapl) == 2
66 | assert "TSLA" in aapl
67 |
68 | def test_quotes_pandas(self):
69 | df = pyTD.market.get_quotes("AAPL")
70 | assert isinstance(df, pd.DataFrame)
71 | assert "AAPL" in df
72 |
73 | assert len(df) == 41
74 |
75 | def test_quotes_bad_symbol(self):
76 | with pytest.raises(ResourceNotFound):
77 | pyTD.market.get_quotes("BADSYMBOL")
78 |
79 | def test_quotes_bad_params(self):
80 | bad = ["AAPL"] * 1000
81 | with pytest.raises(ValueError):
82 | pyTD.market.get_quotes(bad)
83 |
84 |
85 | @pytest.mark.webtest
86 | class TestMarketMovers(object):
87 |
88 | @pytest.mark.xfail(reason="Movers may return empty outside of "
89 | "trading hours.")
90 | def test_movers_json(self):
91 | data = pyTD.market.get_movers("$DJI", output_format='json')
92 | assert isinstance(data, list)
93 | assert len(data) == 10
94 |
95 | @pytest.mark.xfail(reason="Movers may return empty outside of "
96 | "trading hours.")
97 | def test_movers_pandas(self):
98 | data = pyTD.market.get_movers("$DJI")
99 | assert isinstance(data, pd.DataFrame)
100 | assert len(data) == 10
101 |
102 | def test_movers_bad_index(self):
103 | with pytest.raises(ResourceNotFound):
104 | pyTD.market.get_movers("DJI")
105 |
106 | def test_movers_no_params(self):
107 | with pytest.raises(TypeError):
108 | pyTD.market.get_movers()
109 |
110 |
111 | @pytest.mark.webtest
112 | class TestMarketHours(object):
113 |
114 | def test_hours_bad_market(self):
115 | with pytest.raises(ValueError):
116 | pyTD.market.get_market_hours("BADMARKET")
117 |
118 | with pytest.raises(ValueError):
119 | pyTD.market.get_market_hours(["BADMARKET", "EQUITY"])
120 |
121 | def test_hours_default(self):
122 | data = pyTD.market.get_market_hours()
123 |
124 | assert len(data) == 8
125 | assert data.index[0] == "category"
126 |
127 | def test_hours_json(self):
128 | date = now()
129 | data = pyTD.market.get_market_hours("EQUITY", date,
130 | output_format='json')
131 | assert isinstance(data, dict)
132 |
133 | def test_hours_pandas(self):
134 | date = now()
135 | data = pyTD.market.get_market_hours("EQUITY", date)
136 | assert isinstance(data, pd.DataFrame)
137 | assert data.index[0] == "category"
138 |
139 | def test_hours_batch(self):
140 | data = pyTD.market.get_market_hours(["EQUITY", "OPTION"])
141 |
142 | assert len(data) == 8
143 | assert isinstance(data["equity"], pd.Series)
144 |
145 |
146 | @pytest.mark.webtest
147 | class TestOptionChains(object):
148 |
149 | def test_option_chain_no_symbol(self):
150 | with pytest.raises(TypeError):
151 | pyTD.market.get_option_chains()
152 |
153 | def test_option_chain_bad_symbol(self):
154 | with pytest.raises(ResourceNotFound):
155 | pyTD.market.get_option_chains("BADSYMBOL")
156 |
157 | def test_option_chain(self):
158 | data = pyTD.market.get_option_chains("AAPL", output_format='json')
159 |
160 | assert isinstance(data, dict)
161 | assert len(data) == 13
162 | assert data["status"] == "SUCCESS"
163 |
164 | def test_option_chain_call(self):
165 | data = pyTD.market.get_option_chains("AAPL", contract_type="CALL",
166 | output_format='json')
167 |
168 | assert not data["putExpDateMap"]
169 |
170 | def test_option_chain_put(self):
171 | data = pyTD.market.get_option_chains("AAPL", contract_type="PUT",
172 | output_format='json')
173 |
174 | assert not data["callExpDateMap"]
175 |
176 |
177 | @pytest.mark.webtest
178 | class TestPriceHistory(object):
179 |
180 | def test_price_history_no_symbol(self):
181 | with pytest.raises(TypeError):
182 | pyTD.market.get_price_history()
183 |
184 | def test_price_history_default_dates(self):
185 | data = pyTD.market.get_price_history("AAPL", output_format='json')
186 |
187 | assert isinstance(data, list)
188 |
189 | def test_price_history_bad_symbol(self):
190 | with pytest.raises(ResourceNotFound):
191 | pyTD.market.get_price_history("BADSYMBOL")
192 |
193 | def test_price_history_bad_symbols(self):
194 | with pytest.raises(ResourceNotFound):
195 | pyTD.market.get_price_history(["BADSYMBOL", "BADSYMBOL"],
196 | output_format='pandas')
197 |
198 | def test_price_history_json(self):
199 | data = pyTD.market.get_price_history("AAPL", output_format='json')
200 |
201 | assert isinstance(data, list)
202 | assert data[0]["close"] == 172.26
203 | assert data[0]["volume"] == 25555934
204 | assert len(data) > 100
205 |
206 | def test_batch_history_json(self):
207 | syms = ["AAPL", "TSLA", "MSFT"]
208 | data = pyTD.market.get_price_history(syms, output_format='json')
209 |
210 | assert len(data) == 3
211 | assert set(data) == set(syms)
212 |
213 | def test_price_history_pandas(self):
214 | data = pyTD.market.get_price_history("AAPL")
215 |
216 | assert isinstance(data, pd.DataFrame)
217 |
218 | def test_batch_history_pandas(self):
219 | data = pyTD.market.get_price_history(["AAPL", "TSLA", "MSFT"],
220 | output_format='pandas')
221 |
222 | assert isinstance(data, pd.DataFrame)
223 | assert isinstance(data.columns, pd.MultiIndex)
224 |
225 | assert "AAPL" in data.columns
226 | assert "TSLA" in data.columns
227 | assert "MSFT" in data.columns
228 |
229 | assert data.iloc[0].name.date() == datetime.date(2018, 1, 2)
230 |
231 | @pytest.mark.xfail(reason="Odd behavior on travis: wrong dates returned")
232 | def test_history_dates(self):
233 | start = datetime.date(2018, 1, 24)
234 | end = datetime.date(2018, 2, 12)
235 |
236 | data = pyTD.market.get_price_history("AAPL", start_date=start,
237 | end_date=end,
238 | output_format='pandas')
239 |
240 | assert data.iloc[0].name.date() == start
241 | assert data.iloc[-1].name.date() == datetime.date(2018, 2, 9)
242 |
243 | assert pd.infer_freq(data.index) == "B"
244 |
245 |
246 | @pytest.mark.webtest
247 | class TestFundamentals(object):
248 |
249 | def test_fundamentals(self):
250 | data = pyTD.market.get_fundamentals("AAPL")
251 |
252 | assert isinstance(data, pd.DataFrame)
253 | assert len(data) == 46
254 |
--------------------------------------------------------------------------------
/pyTD/market/__init__.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | from pyTD.instruments.base import Instruments
24 | from pyTD.market.hours import MarketHours
25 | from pyTD.market.quotes import Quotes
26 | from pyTD.market.movers import Movers
27 | from pyTD.market.options import Options
28 | from pyTD.market.price_history import PriceHistory
29 |
30 |
31 | def get_fundamentals(*args, **kwargs):
32 | """
33 | Retrieve fundamental data for a diven symbol or CUSIP ID
34 |
35 | Parameters
36 | ----------
37 | symbol: str
38 | A CUSIP ID, symbol, regular expression, or snippet (depends on the
39 | value of the "projection" variable)
40 | output_format: str, default "pandas", optional
41 | Desired output format. "pandas" or "json"
42 | """
43 | kwargs.update({"projection": "fundamental"})
44 | return Instruments(*args, **kwargs).execute()
45 |
46 |
47 | def get_quotes(*args, **kwargs):
48 | """
49 | Function for retrieving quotes from the Get Quotes endpoint.
50 |
51 | Parameters
52 | ----------
53 | symbols : str, array-like object (list, tuple, Series), or DataFrame
54 | Single stock symbol (ticker), array-like object of symbols or
55 | DataFrame with index containing up to 100 stock symbols.
56 | output_format: str, default 'pandas', optional
57 | Desired output format (json or DataFrame)
58 | kwargs: additional request parameters (see _TDBase class)
59 | """
60 | return Quotes(*args, **kwargs).execute()
61 |
62 |
63 | def get_market_hours(*args, **kwargs):
64 | """
65 | Function to retrieve market hours for a given market from the Market
66 | Hours endpoint
67 |
68 | Parameters
69 | ----------
70 | market: str, default EQUITY, optional
71 | The market to retrieve operating hours for
72 | date : string or DateTime object, (defaults to today's date)
73 | Operating date, timestamp. Parses many different kind of date
74 | representations (e.g., 'JAN-01-2015', '1/1/15', 'Jan, 1, 1980')
75 | output_format: str, default 'pandas', optional
76 | Desired output format (json or DataFrame)
77 | kwargs: additional request parameters (see _TDBase class)
78 | """
79 | return MarketHours(*args, **kwargs).execute()
80 |
81 |
82 | def get_movers(*args, **kwargs):
83 | """
84 | Function for retrieving market moveers from the Movers endpoint
85 |
86 | Parameters
87 | ----------
88 | index: str
89 | The index symbol to get movers from
90 | direction: str, default up, optional
91 | Return up or down movers
92 | change: str, default percent, optional
93 | Return movers by percent change or value change
94 | output_format: str, default 'pandas', optional
95 | Desired output format (json or DataFrame)
96 | kwargs: additional request parameters (see _TDBase class)
97 | """
98 | return Movers(*args, **kwargs).execute()
99 |
100 |
101 | def get_option_chains(*args, **kwargs):
102 | """
103 | Function to retrieve option chains for a given symbol from the Option
104 | Chains endpoint
105 |
106 | Parameters
107 | ----------
108 |
109 | contractType: str, default ALL, optional
110 | Desired contract type (CALL, PUT, ALL)
111 | strikeCount: int, optional
112 | Number of strikes to return above and below the at-the-money price
113 | includeQuotes: bool, default False, optional
114 | Include quotes for options in the option chain
115 | strategy: str, default None, optional
116 | Passing a value returns a strategy chain (SINGLE or ANALYTICAL)
117 | interval: int, optional
118 | Strike interval for spread strategy chains
119 | strike: float, optional
120 | Filter options that only have a certain strike price
121 | range: str, optional
122 | Returns options for a given range (ITM, OTM, etc.)
123 | fromDate: str or datetime.datetime object, optional
124 | Only return options after this date
125 | toDate: str or datetime.datetime object, optional
126 | Only return options before this date
127 | volatility: float, optional
128 | Volatility to use in calculations (for analytical strategy chains)
129 | underlyingPrice: float, optional
130 | Underlying price to use in calculations (for analytical strategy
131 | chains)
132 | interestRate: float, optional
133 | Interest rate to use in calculations (for analytical strategy
134 | chains)
135 | daysToExpiration: int, optional
136 | Days to expiration to use in calulations (for analytical
137 | strategy chains)
138 | expMonth: str, optional
139 | Expiration month (format JAN, FEB, etc.) to use in calculations
140 | (for analytical strategy chains), default ALL
141 | optionType: str, optional
142 | Type of contracts to return (S: standard, NS: nonstandard,
143 | ALL: all contracts)
144 | output_format: str, optional, default 'pandas'
145 | Desired output format
146 | api: pyTD.api.api object, optional
147 | A pyTD api object. If not passed, API requestor defaults to
148 | pyTD.api.default_api
149 | kwargs: additional request parameters (see _TDBase class)
150 | """
151 | return Options(*args, **kwargs).execute()
152 |
153 |
154 | def get_price_history(*args, **kwargs):
155 | """
156 | Function to retrieve price history for a given symbol over a given period
157 |
158 | Parameters
159 | ----------
160 | symbols : string, array-like object (list, tuple, Series), or DataFrame
161 | Desired symbols for retrieval
162 | periodType: str, default DAY, optional
163 | The type of period to show
164 | period: int, optional
165 | The number of periods to show
166 | frequencyType: str, optional
167 | The type of frequency with which a new candle is formed
168 | frequency: int, optional
169 | The number of frequencyType to includ with each candle
170 | startDate : string or DateTime object, optional
171 | Starting date, timestamp. Parses many different kind of date
172 | representations (e.g., 'JAN-01-2015', '1/1/15', 'Jan, 1, 1980')
173 | endDate : string or DateTime object, optional
174 | Ending date, timestamp. Parses many different kind of date
175 | representations (e.g., 'JAN-01-2015', '1/1/15', 'Jan, 1, 1980')
176 | extended: str or bool, default 'True'/True, optional
177 | True to return extended hours data, False for regular hours only
178 | output_format: str, default 'pandas', optional
179 | Desired output format (json or DataFrame)
180 | """
181 | return PriceHistory(*args, **kwargs).execute()
182 |
183 |
184 | # def get_history_intraday(symbols, start, end, interval='1m', extended=True,
185 | # output_format='pandas'):
186 | # """
187 | # Function to retrieve intraday price history for a given symbol
188 |
189 | # Parameters
190 | # ----------
191 | # symbols : string, array-like object (list, tuple, Series), or DataFrame
192 | # Desired symbols for retrieval
193 | # startDate : string or DateTime object, optional
194 | # Starting date, timestamp. Parses many different kind of date
195 | # representations (e.g., 'JAN-01-2015', '1/1/15', 'Jan, 1, 1980')
196 | # endDate : string or DateTime object, optional
197 | # Ending date, timestamp. Parses many different kind of date
198 | # representations (e.g., 'JAN-01-2015', '1/1/15', 'Jan, 1, 1980')
199 | # interval: string, default '1m', optional
200 | # Desired interval (1m, 5m, 15m, 30m, 60m)
201 | # needExtendedHoursData: str or bool, default 'True'/True, optional
202 | # True to return extended hours data, False for regular hours only
203 | # output_format: str, default 'pandas', optional
204 | # Desired output format (json or DataFrame)
205 | # """
206 | # result = PriceHistory(symbols, start_date=start, end_date=end,
207 | # extended=extended,
208 | # output_format=output_format).execute()
209 | # if interval == '1m':
210 | # return result
211 | # elif interval == '5m':
212 | # sample = result.index.floor('5T').drop_duplicates()
213 | # return result.reindex(sample, method='ffill')
214 | # elif interval == '15m':
215 | # sample = result.index.floor('15T').drop_duplicates()
216 | # return result.reindex(sample, method='ffill')
217 | # elif interval == '30m':
218 | # sample = result.index.floor('30T').drop_duplicates()
219 | # return result.reindex(sample, method='ffill')
220 | # elif interval == '60m':
221 | # sample = result.index.floor('60T').drop_duplicates()
222 | # return result.reindex(sample, method='ffill')
223 | # else:
224 | # raise ValueError("Interval must be 1m, 5m, 15m, 30m, or 60m.")
225 |
226 |
227 | # def get_history_daily(symbols, start, end, output_format='pandas'):
228 | # return PriceHistory(symbols, start_date=start, end_date=end,
229 | # frequency_type='daily',
230 | # output_format=output_format).execute()
231 |
--------------------------------------------------------------------------------