├── config ├── __init__.py └── config.py ├── tests ├── __init__.py └── test_data.py ├── setup.cfg ├── requirements.txt ├── requirements-dev.txt ├── example.env ├── .idea ├── vcs.xml ├── misc.xml ├── inspectionProfiles │ ├── profiles_settings.xml │ └── Project_Default.xml ├── .gitignore ├── modules.xml └── python-eodhistoricaldata.iml ├── eod_historical_data ├── __init__.py ├── version.py ├── _utils.py └── data.py ├── SECURITY.md ├── ci └── pypi-install.sh ├── .travis.yml ├── LICENSE ├── .gitignore ├── setup.py ├── CONTRIBUTING.md └── README.md /config/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pandas>=0.21 2 | requests>=2.3.0 3 | requests-cache>=0.5.0 4 | python-decouple 5 | aiohttp -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | coverage 2 | codecov 3 | coveralls 4 | flake8 5 | pytest 6 | pytest-cov 7 | wrapt 8 | python-decouple 9 | -------------------------------------------------------------------------------- /example.env: -------------------------------------------------------------------------------- 1 | #save this file as .env file and make sure the dependency python-decouple is included 2 | 3 | EOD_HISTORICAL_API_KEY="OeAFFmMliFG5orCUuwAKQ8l4WWFQ67YX" -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /eod_historical_data/__init__.py: -------------------------------------------------------------------------------- 1 | from .data import (set_envar, get_eod_data, get_dividends, # noqa 2 | get_exchange_symbols, get_exchanges, # noqa 3 | get_currencies, get_indexes) # noqa 4 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /eod_historical_data/version.py: -------------------------------------------------------------------------------- 1 | __author__ = "Femto Trader" 2 | __copyright__ = "Copyright 2017" 3 | __license__ = "MIT" 4 | __version__ = "0.0.1" 5 | __maintainer__ = "femtotrader" 6 | __email__ = "" 7 | __status__ = "Development" 8 | __url__ = "https://github.com/femtotrader/python-eodhistoricaldata" 9 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /../../../../../../:\present work documents\projects\opensource\python-eodhistoricaldata\.idea/dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | We support bug reports and security vulnerabilities reports in this codebase 4 | 5 | ## Reporting a Vulnerability 6 | if you find any vulnerability please add it to our issues and it will be resolved as soon as possible. 7 | or else send the bug reports to this email address mobiusndou@gmail.com patches will be developed ASAP. 8 | -------------------------------------------------------------------------------- /config/config.py: -------------------------------------------------------------------------------- 1 | from decouple import config 2 | import os 3 | class Config: 4 | EOD_HISTORICAL_DATA_API_KEY_ENV_VAR: str = os.getenv('EOD_HISTORICAL_API_KEY') or config('EOD_HISTORICAL_API_KEY') 5 | EOD_HISTORICAL_DATA_API_KEY_DEFAULT: str = os.getenv('EOD_HISTORICAL_API_KEY') or config('EOD_HISTORICAL_API_KEY') 6 | EOD_HISTORICAL_DATA_API_URL: str = "https://eodhistoricaldata.com/api" 7 | DEBUG: bool = False 8 | -------------------------------------------------------------------------------- /ci/pypi-install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | pip install pip --upgrade 4 | pip install numpy=="$NUMPY" lxml 5 | pip install -r requirements-dev.txt 6 | 7 | if [[ "$PANDAS" == "MASTER" ]]; then 8 | PRE_WHEELS="https://7933911d6844c6c53a7d-47bd50c35cd79bd838daf386af554a83.ssl.cf2.rackcdn.com" 9 | pip install --pre --upgrade --timeout=60 -f "$PRE_WHEELS" pandas 10 | else 11 | pip install pandas=="$PANDAS" 12 | fi 13 | -------------------------------------------------------------------------------- /.idea/python-eodhistoricaldata.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: bionic 2 | language: python 3 | 4 | matrix: 5 | fast_finish: true 6 | include: 7 | - python: 3.6 8 | env: PANDAS=0.23 NUMPY=1.14 9 | - python: 3.7 10 | env: PANDAS=0.25 NUMPY=1.17 11 | allow_failures: 12 | - python: 3.7 13 | env: PANDAS="MASTER" NUMPY=1.17 14 | 15 | install: 16 | - source ci/pypi-install.sh; 17 | - pip list 18 | - python setup.py install 19 | 20 | script: 21 | - export EOD_HISTORICAL_API_KEY=$EOD_HISTORICAL_API_KEY 22 | - pytest -s --cov=eod_historical_data --cov-report xml:/tmp/cov-eod_historical_data.xml --junitxml=/tmp/eod_historical_data.xml 23 | - flake8 --version 24 | - flake8 eod_historical_data tests 25 | 26 | after_success: 27 | - coveralls 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 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 all 13 | 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | venv/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # SageMath parsed files 81 | *.sage.py 82 | 83 | # dotenv 84 | .env 85 | 86 | # virtualenv 87 | .venv 88 | venv/ 89 | ENV/ 90 | 91 | # Spyder project settings 92 | .spyderproject 93 | .spyproject 94 | 95 | # Rope project settings 96 | .ropeproject 97 | 98 | # mkdocs documentation 99 | /site 100 | 101 | # mypy 102 | .mypy_cache/ 103 | 104 | *.sqlite 105 | .DS_Store 106 | 107 | # Pycharm 108 | .idea -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 40 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from setuptools import setup, find_packages # Always prefer setuptools over distutils 5 | from os import path 6 | import io 7 | 8 | here = path.abspath(path.dirname(__file__)) 9 | 10 | try: 11 | import pypandoc 12 | long_description = pypandoc.convert('README.md', 'rst') 13 | except(IOError, ImportError): 14 | print("Can't import pypandoc - using README.md without converting to RST") 15 | long_description = open('README.md').read() 16 | 17 | NAME = 'eod_historical_data' 18 | with io.open(path.join(here, NAME, 'version.py'), 'rt', encoding='UTF-8') as f: 19 | exec(f.read()) 20 | 21 | install_requires = [] 22 | with open("./requirements.txt") as f: 23 | install_requires = f.read().splitlines() 24 | with open("./requirements-dev.txt") as f: 25 | tests_require = f.read().splitlines() 26 | 27 | setup( 28 | name=NAME, 29 | 30 | # Versions should comply with PEP440. For a discussion on single-sourcing 31 | # the version across setup.py and the project code, see 32 | # https://packaging.python.org/en/latest/development.html#single-sourcing-the-version 33 | # version='0.0.2', 34 | version=__version__, 35 | 36 | description='End Of Day historical data using Python, Requests, Pandas', 37 | long_description=long_description, 38 | 39 | # The project's main homepage. 40 | url=__url__, 41 | 42 | # Author details 43 | author=__author__, 44 | author_email=__email__, 45 | 46 | # Choose your license 47 | license=__license__, 48 | 49 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers 50 | classifiers=[ 51 | 'Development Status :: 3 - Alpha', 52 | 'Environment :: Console', 53 | 'Intended Audience :: Developers', 54 | 'Intended Audience :: Science/Research', 55 | 'Operating System :: OS Independent', 56 | 'Intended Audience :: Financial and Insurance Industry', 57 | 'Programming Language :: Cython', 58 | 'Programming Language :: Python', 59 | 'Programming Language :: Python :: 3.5', 60 | 'Programming Language :: Python :: 3.6', 61 | 'Topic :: Scientific/Engineering', 62 | 'Topic :: Office/Business :: Financial', 63 | 'License :: OSI Approved :: MIT License', 64 | ], 65 | 66 | keywords='python trading data stock index', 67 | install_requires=install_requires, 68 | packages=find_packages(exclude=["contrib", "docs", "tests*"]), 69 | test_suite="tests", 70 | tests_require=tests_require, 71 | zip_safe=False, 72 | ) 73 | -------------------------------------------------------------------------------- /tests/test_data.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import sentinel 2 | import pandas as pd 3 | from eod_historical_data import (set_envar, 4 | get_eod_data, 5 | get_dividends, 6 | get_exchange_symbols, 7 | get_exchanges, get_currencies, get_indexes) 8 | import datetime 9 | import requests_cache 10 | 11 | pd.set_option("max_rows", 10) 12 | 13 | # Cache session (to avoid too much data consumption) 14 | expire_after = datetime.timedelta(days=1) 15 | session = requests_cache.CachedSession(cache_name='cache', backend='sqlite', 16 | expire_after=expire_after) 17 | 18 | # Get API key 19 | # from environment variable 20 | # bash> export EOD_HISTORICAL_API_KEY="YOURAPI" 21 | api_key = set_envar() 22 | # api_key = "YOURAPI" 23 | 24 | 25 | def test_get_eod_data_no_date(): 26 | df = get_eod_data("AAPL", "US", api_key=api_key, session=session) 27 | print(df) 28 | # Note if df is Sentinel it means that the request was sent through but the response 29 | # was forbidden indicating that the APi Key may not have been authorized to perform the 30 | # Operation 31 | if df is not sentinel: 32 | assert df.index.name == "Date" 33 | 34 | 35 | def test_get_eod_data_with_date(): 36 | df = get_eod_data("AAPL", "US", start="2020-02-01", end="2020-02-10", 37 | api_key=api_key, session=session) 38 | print(df) 39 | if df is not sentinel: 40 | assert df.index.name == "Date" 41 | assert df.index[0] != "" 42 | 43 | 44 | def test_get_dividends(): 45 | df = get_dividends("AAPL", "US", start="2020-02-01", end="2020-02-10", 46 | api_key=api_key, session=session) 47 | print(df) 48 | if df is not sentinel: 49 | assert df.index.name == "Date" 50 | 51 | 52 | def test_get_exchange_symbols(): 53 | df = get_exchange_symbols(exchange_code="US", 54 | api_key=api_key, session=session) 55 | print(df) 56 | if df is not sentinel: 57 | assert df.index.name == "Code" 58 | assert "AAPL" in df.index 59 | 60 | 61 | def test_get_exchanges(): 62 | df = get_exchanges() 63 | print(df) 64 | if df is not sentinel: 65 | assert df.index.name == "ID" 66 | assert "US" in df["Exchange Code"].unique() 67 | 68 | 69 | def test_get_currencies(): 70 | df = get_currencies() 71 | print(df) 72 | if df is not sentinel: 73 | assert df.index.name == "ID" 74 | assert "USD" in df["Currency Code"].unique() 75 | 76 | 77 | def test_get_indexes(): 78 | df = get_indexes() 79 | print(df) 80 | if df is not sentinel: 81 | assert df.index.name == "ID" 82 | assert "GSPC" in df["Code"].unique() 83 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Python EOD Historical Data 2 | 3 | :+1::tada: First off, thanks for taking the time to contribute! :tada::+1: 4 | 5 | The following is a set of guidelines for contributing to EOD Historical Data and its packages, 6 | which are hosted in the [EOD Orgnization](https://github.com/EodHistoricalData) on GitHub. 7 | These are mostly guidelines, not rules. Use your best judgment, 8 | and feel free to propose changes to this document in a pull request. 9 | 10 | 11 | ## Code of Conduct 12 | 13 | Members of this project and everyone who participates in it are expected to behave well, specifically 14 | not to use bad language when communicating with others, to be helpful and considerate. 15 | 16 | ## I don't want to read this whole thing I just have a question!!! 17 | **Note:** submit an issue to ask a question 18 | 19 | ## How can I Contribute 20 | #. Fork the repo, develop and test your code changes, add docs. 21 | #. Make sure that your commit messages clearly describe the changes. 22 | #. Send a pull request. (Please Read: `Faster Pull Request Reviews`_) 23 | 24 | *************** 25 | Adding Features 26 | *************** 27 | 28 | In order to add a feature: 29 | 30 | - The feature must be documented in both the API and narrative 31 | documentation. 32 | 33 | - The feature must work fully on the following Python versions: 34 | 3.7, 3.8, 3.9 and on both Linux and Windows. 35 | 36 | - The feature must not add unnecessary dependencies (where 37 | "unnecessary" is of course subjective, but new dependencies should 38 | be discussed). 39 | 40 | **************************** 41 | Using a Development Checkout 42 | **************************** 43 | 44 | You'll have to create a development environment using a Git checkout: 45 | 46 | - While logged into your GitHub account, navigate to the 47 | ``python-eodhistoricaldata`` `repo`_ on GitHub. 48 | 49 | - Fork and clone the ``python-eodhistoricaldata`` repository to your GitHub account by 50 | clicking the "Fork" button. 51 | 52 | - Clone your fork of ``python-eodhistoricaldata`` from your GitHub account to your local 53 | computer, substituting your account username and specifying the destination 54 | as ``python-eodhistoricaldata``. E.g.:: 55 | 56 | $ cd ${HOME} 57 | $ git clone git@github.com:USERNAME/python-eodhistoricaldata.git python-eodhistoricaldata 58 | $ cd python-eodhistoricaldata 59 | # Configure remotes such that you can pull changes from the python-eodhistoricaldata 60 | # repository into your local repository. 61 | $ git remote add upstream git@github.com:EodHistoricalData/python-eodhistoricaldata 62 | # fetch and merge changes from upstream into master 63 | $ git fetch upstream 64 | $ git merge upstream/master 65 | 66 | Now your local repo is set up such that you will push changes to your GitHub 67 | repo, from which you can submit a pull request. 68 | 69 | 70 | ************ 71 | Coding Style 72 | ************ 73 | 74 | - PEP8 compliance 75 | 76 | ************ 77 | Running code Tests 78 | ************ 79 | 80 | use the py.test command on the root of your local repo 81 | 82 | 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | !! We recommend using our official library, which is regularly updated and comes with detailed documentation: 2 | https://github.com/EodHistoricalData/EODHD-APIs-Python-Financial-Library 3 | 4 | 5 | 6 | # Python EOD Historical Data 7 | 8 | A library to download data from EOD historical data https://eodhd.com/ using: 9 | - [Python](https://www.python.org/) 10 | - [Requests](http://docs.python-requests.org/) / [Requests-cache](http://requests-cache.readthedocs.io/) 11 | - [Pandas](http://pandas.pydata.org/) 12 | 13 | ## Installation 14 | 15 | ### Install the latest development version 16 | 17 | ```bash 18 | $ pip install git+https://github.com/femtotrader/python-eodhistoricaldata.git 19 | ``` 20 | 21 | or 22 | 23 | ```bash 24 | $ git clone https://github.com/femtotrader/python-eodhistoricaldata.git 25 | $ python setup.py install 26 | ``` 27 | 28 | ## Usage 29 | 30 | Environment variable `EOD_HISTORICAL_API_KEY` should be defined using: 31 | 32 | ```bash 33 | export EOD_HISTORICAL_API_KEY="YOUR_API" 34 | 35 | or setup and .env file and write your api key 36 | like EOD_HISTORICAL_API_KEY="YOUR_API" 37 | see exampl.env 38 | ``` 39 | 40 | 41 | You can download data simply using 42 | 43 | ```python 44 | In [1]: import pandas as pd 45 | In [2]: pd.set_option("max_rows", 10) 46 | In [3]: from eod_historical_data import get_eod_data 47 | In [4]: df = get_eod_data("AAPL", "US") 48 | In [5]: df 49 | Out[1]: 50 | Open High Low Close Adjusted_close \ 51 | Date 52 | 2000-01-03 3.7455 4.0179 3.6317 3.9978 3.9978 53 | 2000-01-04 3.8661 3.9509 3.6138 3.6607 3.6607 54 | 2000-01-05 3.7054 3.9487 3.6786 3.7143 3.7143 55 | 2000-01-06 3.7902 3.8214 3.3929 3.3929 3.3929 56 | 2000-01-07 3.4464 3.6071 3.4107 3.5536 3.5536 57 | ... ... ... ... ... ... 58 | 2017-05-26 154.0000 154.2400 153.3100 153.6100 153.6100 59 | 2017-05-30 153.4200 154.4300 153.3300 153.6700 153.6700 60 | 2017-05-31 153.9700 154.1700 152.3800 152.7600 152.7600 61 | 2017-06-01 153.1700 153.3300 152.2200 153.1800 153.1800 62 | 2017-06-02 153.6000 155.4500 152.8900 155.4500 155.4500 63 | 64 | Volume 65 | Date 66 | 2000-01-03 133949200.0 67 | 2000-01-04 128094400.0 68 | 2000-01-05 194580400.0 69 | 2000-01-06 191993200.0 70 | 2000-01-07 115183600.0 71 | ... ... 72 | 2017-05-26 21927600.0 73 | 2017-05-30 20126900.0 74 | 2017-05-31 24451200.0 75 | 2017-06-01 16274200.0 76 | 2017-06-02 25163841.0 77 | 78 | [4382 rows x 6 columns] 79 | ``` 80 | 81 | but if you want to avoid too much data consumption, you can use a cache mechanism. 82 | 83 | 84 | ```python 85 | In [1]: import datetime 86 | In [2]: import requests_cache 87 | In [3]: expire_after = datetime.timedelta(days=1) 88 | In [4]: session = requests_cache.CachedSession(cache_name='cache', backend='sqlite', expire_after=expire_after) 89 | In [5]: df = get_eod_data("AAPL", "US", session=session) 90 | ``` 91 | 92 | See [tests directory](https://github.com/femtotrader/python-eodhistoricaldata/tree/master/tests) for example of other API endpoints. 93 | 94 | ## Credits 95 | 96 | - Original version by [FemtoTrader](https://github.com/femtotrader/) 2017-2021 97 | - Idea to create this project [came from this issue](https://github.com/pydata/pandas-datareader/issues/315) (Thanks [@deios0](https://github.com/deios0) ) 98 | - Code was inspired by [pandas-datareader](http://pandas-datareader.readthedocs.io/) 99 | 100 | -------------------------------------------------------------------------------- /eod_historical_data/_utils.py: -------------------------------------------------------------------------------- 1 | from sqlite3 import Timestamp 2 | from typing import Optional, Union, Dict, Tuple, Callable 3 | import functools 4 | import requests 5 | from datetime import date, datetime 6 | import traceback 7 | import pandas as pd 8 | import pandas.api.types as _types 9 | from urllib.parse import urlencode 10 | from requests.exceptions import RetryError, ConnectTimeout 11 | from config.config import Config 12 | 13 | config_instance: Config = Config() 14 | # NOTE do not remove 15 | 16 | 17 | Sanitize_Type = Tuple[Union[Timestamp, datetime], Union[Timestamp, datetime]] 18 | Handle_Request_Type = Callable[..., Optional[pd.DataFrame]] 19 | 20 | 21 | def _init_session(session: Optional[requests.Session]) -> requests.Session: 22 | """ 23 | Returns a requests.Session (or CachedSession) 24 | """ 25 | return requests.Session() if session is None else session 26 | 27 | 28 | def _url(url: str, params: Dict[str, str]) -> str: 29 | """ 30 | Returns long url with parameters 31 | https://mydomain.com?param1=...¶m2=... 32 | """ 33 | return "{}?{}".format(url, urlencode(params)) if isinstance(params, dict) and len(params) > 0 else url 34 | 35 | 36 | def _format_date(dt: Optional[datetime]) -> Optional[str]: 37 | """ 38 | Returns formatted date 39 | """ 40 | return None if dt is None else dt.strftime("%Y-%m-%d") 41 | 42 | 43 | def _sanitize_dates(start: Union[int, date, datetime], end: Union[int, date, datetime]) -> Sanitize_Type: 44 | """ 45 | Return (datetime_start, datetime_end) tuple 46 | """ 47 | if start and end: 48 | if start > end: 49 | raise ValueError("end must be after start") 50 | else: 51 | raise ValueError("start and or end must contain valid int. date or datetime object") 52 | 53 | start = datetime(start, 1, 1) if _types.is_number(start) else pd.to_datetime(start) 54 | end = datetime(end, 1, 1) if _types.is_number(end) else pd.to_datetime(end) 55 | 56 | return start, end 57 | 58 | 59 | def _handle_request_errors(func: Handle_Request_Type) -> Optional[Handle_Request_Type]: 60 | @functools.wraps(func) 61 | def wrapper(*args, **kwargs): 62 | try: 63 | return func(*args, **kwargs) 64 | except ConnectionError: 65 | if config_instance.DEBUG is True: 66 | print(traceback.format_exc()) 67 | else: 68 | print("Connection Error") 69 | return None 70 | except RetryError: 71 | if config_instance.DEBUG is True: 72 | print(traceback.format_exc()) 73 | else: 74 | print("Connection Error") 75 | return None 76 | except ConnectTimeout: 77 | if config_instance.DEBUG is True: 78 | print(traceback.format_exc()) 79 | else: 80 | print("Connection Error") 81 | return None 82 | 83 | return wrapper 84 | 85 | 86 | def _handle_environ_error(func: Handle_Request_Type) -> Optional[Handle_Request_Type]: 87 | @functools.wraps(func) 88 | def wrapper(*args, **kwargs): 89 | try: 90 | api_key: Optional[str] = kwargs.get('api_key') 91 | assert api_key is not None 92 | assert api_key != "" 93 | return func(*args, **kwargs) 94 | except AssertionError: 95 | raise EnvironNotSet("Environment not set, see readme.md on how to setup your environment variables") 96 | 97 | return wrapper 98 | 99 | 100 | # Errors 101 | 102 | class RemoteDataError(IOError): 103 | """ 104 | Remote data exception 105 | """ 106 | pass 107 | 108 | 109 | class EnvironNotSet(Exception): 110 | """ 111 | raised when environment variables are not set 112 | """ 113 | pass 114 | 115 | 116 | api_key_not_authorized: int = 403 117 | -------------------------------------------------------------------------------- /eod_historical_data/data.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, date 2 | from io import StringIO 3 | from typing import Union, Optional, Dict, Tuple 4 | from unittest.mock import sentinel 5 | 6 | import aiohttp as io 7 | import pandas as pd 8 | import requests 9 | 10 | from config.config import Config 11 | from ._utils import (_init_session, _format_date, 12 | _sanitize_dates, _url, RemoteDataError, _handle_request_errors, _handle_environ_error, 13 | api_key_not_authorized) 14 | 15 | config_data: Config = Config() 16 | 17 | EOD_HISTORICAL_DATA_API_KEY_ENV_VAR: str = config_data.EOD_HISTORICAL_DATA_API_KEY_ENV_VAR 18 | EOD_HISTORICAL_DATA_API_KEY_DEFAULT: str = config_data.EOD_HISTORICAL_DATA_API_KEY_DEFAULT 19 | EOD_HISTORICAL_DATA_API_URL: str = config_data.EOD_HISTORICAL_DATA_API_URL 20 | 21 | Start_END_Type = Optional[Union[str, int, date, datetime]] 22 | 23 | 24 | def set_envar() -> str: 25 | return EOD_HISTORICAL_DATA_API_KEY_ENV_VAR 26 | 27 | 28 | def _api_key_not_authorized_message(): 29 | print(f"API Key Restricted, Try upgrading your API Key: {__name__}") 30 | return sentinel 31 | 32 | 33 | def _create_params(symbol: str, exchange: str, start: Start_END_Type, 34 | end: Start_END_Type, api_key: str) -> Tuple[str, Dict[str, str]]: 35 | """ 36 | **create_params** 37 | will create parameters to pass into request session 38 | """ 39 | symbol_exchange: str = f"{symbol}.{exchange}" 40 | # Takes date, datetime, str , or int and returns a valid date objects or TimeStamps 41 | start, end = _sanitize_dates(start, end) 42 | endpoint: str = f"/eod/{symbol_exchange}" 43 | url: str = EOD_HISTORICAL_DATA_API_URL + endpoint 44 | params: dict = { 45 | "api_token": api_key, 46 | "from": _format_date(start), 47 | "to": _format_date(end) 48 | } 49 | return url, params 50 | 51 | 52 | def _create_request(api_key: str, end: Start_END_Type, start: Start_END_Type, exchange: str, symbol: str, 53 | session: requests.Session) -> Tuple[Dict[str, str], requests.Response, str]: 54 | """ 55 | **_create_request** 56 | will create a request using request library to eod endpoint to fetch data 57 | """ 58 | url, params = _create_params(symbol=symbol, exchange=exchange, start=start, end=end, api_key=api_key) 59 | r: requests.Response = session.get(url, params=params) 60 | if config_data.DEBUG: 61 | print(f"url = {url} params = {params}") 62 | print(f'status code : {r.status_code}') 63 | return params, r, url 64 | 65 | 66 | @_handle_environ_error 67 | @_handle_request_errors 68 | def get_eod_data(symbol: str, exchange: str, start: Start_END_Type = None, end: Start_END_Type = None, 69 | api_key: str = EOD_HISTORICAL_DATA_API_KEY_DEFAULT, 70 | session: Optional[requests.Session] = None) -> Optional[pd.DataFrame]: 71 | """ 72 | **get_eod_data** 73 | 74 | PARAMETERS 75 | symbol: (str) -> Ticker Symbol 76 | exchange: (str) -> Exchange Code 77 | start: (str, int, date, datetime) -> start date or integer representing Year 78 | end: (str,int, date, datetime) -> end date or integer representing Year 79 | api_key: (str) -> EOD Historical API Key 80 | session: (str) -> Request Session Object 81 | 82 | EXCEPTIONS: 83 | RemoteDataError -> will be raised if data cannot be returned from EOD for any reason 84 | ValueError -> Will be raised if either start or end do not contain Valid Data 85 | 86 | returns -> DataFrame containing End oF Day Data for the Symbol 87 | """ 88 | 89 | params, r, url = _create_request(api_key=api_key, end=end, start=start, 90 | exchange=exchange, session=session, symbol=symbol) 91 | 92 | if r.status_code == requests.codes.ok: 93 | # NOTE engine='c' which is default does not support skip footer 94 | df: Optional[pd.DataFrame] = pd.read_csv(StringIO(r.text), engine='python', 95 | skipfooter=0, parse_dates=[0], index_col=0) 96 | return df 97 | elif r.status_code == api_key_not_authorized: 98 | _api_key_not_authorized_message() 99 | else: 100 | # replacing api token so it does not show in debug messages 101 | params["api_token"] = "API TOKEN IS SECRET" 102 | raise RemoteDataError(r.status_code, r.reason, _url(url, params)) 103 | 104 | 105 | @_handle_environ_error 106 | @_handle_request_errors 107 | async def get_eod_data_async(symbol: str, exchange: str, start: Start_END_Type = None, 108 | end: Start_END_Type = None, 109 | api_key: str = EOD_HISTORICAL_DATA_API_KEY_DEFAULT) -> \ 110 | Optional[Union[pd.TextFileReader, pd.DataFrame]]: 111 | """ 112 | **get_eod_data_async** 113 | 114 | PARAMETERS 115 | symbol: (str) -> Ticker Symbol 116 | exchange: (str) -> Exchange Code 117 | start: (str, int, date, datetime) -> start date or integer representing Year 118 | end: (str,int, date, datetime) -> end date or integer representing Year 119 | api_key: (str) -> EOD Historical API Key 120 | session: (str) -> Request Session Object 121 | 122 | EXCEPTIONS: 123 | RemoteDataError -> will be raised if data cannot be returned from EOD for any reason 124 | ValueError -> Will be raised if either start or end do not contain Valid Data 125 | 126 | returns -> DataFrame containing End oF Day Data for the Symbol 127 | """ 128 | 129 | url, params = _create_params(symbol=symbol, exchange=exchange, start=start, end=end, api_key=api_key) 130 | 131 | async with io.ClientSession() as session: 132 | async with session.get(url, params=params) as response: 133 | if response.status == 200: 134 | response_data = await response.text() 135 | df: Union[pd.TextFileReader, pd.DataFrame] = pd.read_csv(StringIO(response_data), engine='python', 136 | skipfooter=0, 137 | parse_dates=[0], index_col=0) 138 | return df 139 | elif response.status == api_key_not_authorized: 140 | _api_key_not_authorized_message() 141 | else: 142 | # replacing api token so it does not show in debug messages 143 | params["api_token"] = "API TOKEN IS SECRET" 144 | raise RemoteDataError(response.status, response.reason, _url(url, params)) 145 | 146 | 147 | @_handle_environ_error 148 | @_handle_request_errors 149 | def get_dividends(symbol: str, exchange: str, start: Start_END_Type = None, end: Start_END_Type = None, 150 | api_key: str = EOD_HISTORICAL_DATA_API_KEY_DEFAULT, 151 | session: Optional[requests.Session] = None) -> Optional[Union[pd.TextFileReader, pd.DataFrame]]: 152 | """ 153 | **get_dividends** 154 | 155 | """ 156 | params, r, url = _create_request(api_key=api_key, end=end, start=start, 157 | exchange=exchange, session=session, symbol=symbol) 158 | 159 | if r.status_code == requests.codes.ok: 160 | # NOTE engine='c' which is default does not support skip footer 161 | df: Union[pd.TextFileReader, pd.DataFrame] = pd.read_csv(StringIO(r.text), engine='python', 162 | skipfooter=0, 163 | parse_dates=[0], index_col=0) 164 | assert len(df.columns) == 1 165 | ts = df["Dividends"] 166 | return ts 167 | elif r.status_code == api_key_not_authorized: 168 | _api_key_not_authorized_message() 169 | else: 170 | params["api_token"] = "API TOKEN IS SECRET" 171 | raise RemoteDataError(r.status_code, r.reason, _url(url, params)) 172 | 173 | 174 | @_handle_environ_error 175 | @_handle_request_errors 176 | async def get_dividends_async(symbol: str, exchange: str, start: Union[str, int] = None, 177 | end: Union[str, int] = None, 178 | api_key: str = EOD_HISTORICAL_DATA_API_KEY_DEFAULT) -> Optional[pd.DataFrame]: 179 | """ 180 | Returns dividends 181 | """ 182 | url, params = _create_params(symbol=symbol, exchange=exchange, start=start, end=end, api_key=api_key) 183 | 184 | async with io.ClientSession() as session: 185 | async with session.get(url, params=params) as response: 186 | if response.status == 200: 187 | response_data = await response.text() 188 | df: Union[pd.TextFileReader, pd.DataFrame] = pd.read_csv(StringIO(response_data), engine='python', 189 | skipfooter=0, 190 | parse_dates=[0], index_col=0) 191 | assert len(df.columns) == 1 192 | ts = df["Dividends"] 193 | return ts 194 | elif response.status == api_key_not_authorized: 195 | _api_key_not_authorized_message() 196 | else: 197 | params["api_token"] = "API TOKEN IS SECRET" 198 | raise RemoteDataError(response.status, response.reason, _url(url, params)) 199 | 200 | 201 | @_handle_environ_error 202 | @_handle_request_errors 203 | def get_exchange_symbols(exchange_code: str, 204 | api_key: str = EOD_HISTORICAL_DATA_API_KEY_DEFAULT, 205 | session: Union[requests.Session, None] = None) -> Optional[pd.DataFrame]: 206 | """ 207 | Returns list of symbols for a given exchange 208 | """ 209 | session: requests.Session = _init_session(session) 210 | endpoint: str = f"/exchanges/{exchange_code}" 211 | url: str = EOD_HISTORICAL_DATA_API_URL + endpoint 212 | params: dict = {"api_token": api_key} 213 | 214 | r: requests.Response = session.get(url, params=params) 215 | if config_data.DEBUG: 216 | print(f'status code : {r.status_code}') 217 | if r.status_code == requests.codes.ok: 218 | df: Union[pd.TextFileReader, pd.DataFrame] = pd.read_csv(StringIO(r.text), engine='python', skipfooter=0, 219 | index_col=0) 220 | return df 221 | elif r.status_code == api_key_not_authorized: 222 | _api_key_not_authorized_message() 223 | else: 224 | # replacing api token so it does not show in debug messages 225 | params["api_token"] = "API TOKEN IS SECRET" 226 | raise RemoteDataError(r.status_code, r.reason, _url(url, params)) 227 | 228 | 229 | @_handle_environ_error 230 | @_handle_request_errors 231 | async def get_exchange_symbols_async(exchange_code: str, 232 | api_key: str = EOD_HISTORICAL_DATA_API_KEY_DEFAULT) -> Optional[pd.DataFrame]: 233 | """ 234 | Returns list of symbols for a given exchange 235 | """ 236 | endpoint: str = f"/exchanges/{exchange_code}" 237 | url: str = EOD_HISTORICAL_DATA_API_URL + endpoint 238 | params: dict = { 239 | "api_token": api_key 240 | } 241 | async with io.ClientSession() as session: 242 | async with session.get(url, params=params) as response: 243 | if response.status == 200: 244 | response_data = await response.text() 245 | df: Union[pd.TextFileReader, pd.DataFrame] = pd.read_csv(StringIO(response_data), engine='python', 246 | skipfooter=0, index_col=0) 247 | return df 248 | elif response.status == api_key_not_authorized: 249 | _api_key_not_authorized_message() 250 | else: 251 | # replacing api token so it does not show in debug messages 252 | params["api_token"] = "API TOKEN IS SECRET" 253 | raise RemoteDataError(response.status, response.reason, _url(url, params)) 254 | 255 | 256 | def get_exchanges() -> pd.DataFrame: 257 | """ 258 | Returns list of exchanges 259 | https://eodhistoricaldata.com/knowledgebase/list-supported-exchanges/ 260 | """ 261 | data: str = """ 262 | ID Exchange Name Exchange Code 263 | 1 Munich Exchange MU 264 | 2 Berlin Exchange BE 265 | 3 Frankfurt Exchange F 266 | 4 Stuttgart Exchange STU 267 | 5 Mexican Exchange MX 268 | 6 Hanover Exchange HA 269 | 8 Australian Exchange AU 270 | 9 Singapore Exchange SG 271 | 10 Indexes INDX 272 | 11 USA Stocks US 273 | 12 Kuala Lumpur Exchange KLSE 274 | 13 Funds FUND 275 | 14 Bombay Exchange BSE 276 | 15 Dusseldorf Exchange DU 277 | 16 London Exchange LSE 278 | 17 Euronext Paris PA 279 | 18 XETRA Exchange XETRA 280 | 19 NSE (India) NSE 281 | 20 Hong Kong Exchange HK 282 | 21 Borsa Italiana MI 283 | 22 SIX Swiss Exchange SW 284 | 23 Hamburg Exchange HM 285 | 24 Toronto Exchange TO 286 | 25 Stockholm Exchange ST 287 | 26 Oslo Stock Exchange OL 288 | 27 Euronext Amsterdam AS 289 | 28 Coppenhagen Exchange CO 290 | 29 Euronext Lisbon LS 291 | 30 Korea Stock Exchange KO 292 | 31 Shanghai Exchange SS 293 | 32 Taiwan Exchange TW 294 | 33 Sao Paolo Exchange SA 295 | 34 Euronext Brussels BR 296 | 35 Madrid Exchange MC 297 | 36 Vienna Exchange VI 298 | 37 New Zealand Exchange NZ 299 | 38 FOREX FX 300 | 39 London IL IL 301 | 40 Irish Exchange IR 302 | 41 MICEX Russia MCX 303 | 42 OTC Market OTC 304 | 43 ETF-Euronext NX 305 | 44 Johannesburg Exchange JSE""" 306 | df: Union[pd.TextFileReader, pd.DataFrame] = pd.read_csv(StringIO(data), sep="\t") 307 | df: pd.DataFrame = df.set_index("ID") 308 | return df 309 | 310 | 311 | def get_currencies() -> pd.DataFrame: 312 | """ 313 | Returns list of supported currencies 314 | https://eodhistoricaldata.com/knowledgebase/list-supported-currencies/ 315 | """ 316 | data: str = """ID Exchange Code Currency Code 317 | 1 FX USD 318 | 2 FX EUR 319 | 3 FX RUB 320 | 4 FX GBP 321 | 5 FX CNY 322 | 6 FX JPY 323 | 7 FX SGD 324 | 8 FX INR 325 | 9 FX CHF 326 | 10 FX AUD 327 | 11 FX CAD 328 | 12 FX HKD 329 | 13 FX MYR 330 | 14 FX NOK 331 | 15 FX NZD 332 | 16 FX ZAR 333 | 17 FX SEK 334 | 18 FX DKK 335 | 19 FX BRL 336 | 20 FX ZAC 337 | 21 FX MXN 338 | 22 FX TWD 339 | 23 FX KRW 340 | 24 FX CLP 341 | 25 FX CZK 342 | 26 FX HUF 343 | 27 FX IDR 344 | 28 FX ISK 345 | 29 FX MXV 346 | 30 FX PLN 347 | 31 FX TRY 348 | 32 FX UYU 349 | 33 FX BTC""" 350 | df: Union[pd.TextFileReader, pd.DataFrame] = pd.read_csv(StringIO(data), sep="\t") 351 | df: pd.DataFrame = df.set_index("ID") 352 | return df 353 | 354 | 355 | def get_indexes() -> pd.DataFrame: 356 | """ 357 | Returns list of supported indexes 358 | https://eodhistoricaldata.com/knowledgebase/list-supported-indexes/ 359 | """ 360 | data: str = """ID Exchange Code Code Index Name 361 | 1 INDX GSPC S&P 500 362 | 2 INDX GDAXI DAX Index 363 | 3 INDX SSEC Shanghai Composite Index (China) 364 | 4 INDX MERV MERVAL Index (Argentina) 365 | 5 INDX FTSE FTSE 100 Index (UK) 366 | 6 INDX AORD All Ordinaries Index (Australia) 367 | 7 INDX BSESN BSE 30 Sensitivity Index (SENSEX) 368 | 8 INDX VIX VIX S&P 500 Volatility Index 369 | 9 INDX HSI Hang Seng Index (Hong Kong) 370 | 10 INDX GSPTSE S&P TSX Composite Index (Canada) 371 | 11 INDX FCHI CAC 40 Index 372 | 12 INDX TA100 Tel Aviv 100 Index (Israel 373 | 13 INDX CYC Morgan Stanley Cyclical Index 374 | 14 INDX IIX Interactive Week Internet Index 375 | 15 INDX CMR Morgan Stanley Consumer Index 376 | 16 INDX GOX CBOE Gold Inde 377 | 17 INDX RTS_RS RTSI Index 378 | 18 INDX GD_AT Athens Composite Inde 379 | 19 INDX FTSEMIB_MI Untitled Dataset 2015-07-13 20:00:12 380 | 20 INDX WILREIT Wilshire US REIT Inde 381 | 21 INDX W5KMCG Wilshire US Mid Cap Growt 382 | 22 INDX IBEX IBEX 35 Index 383 | 23 INDX W5KLCV Wilshire US Large Cap Valu 384 | 24 INDX SSMI Swiss Market Index 385 | 25 INDX OEX S&P 100 Inde 386 | 26 INDX RUI Russell 1000 Inde 387 | 27 INDX XAX NYSE AMEX Composite Inde 388 | 28 INDX WILRESI Wilshire US Real Estate Securities Inde 389 | 29 INDX NZ50 NZSE 50 (New Zealand) 390 | 30 INDX UTY PHLX Utility Sector Inde 391 | 31 INDX CSE Colombo All Shares Index (Sri Lanka 392 | 32 INDX XOI NYSE AMEX Oil Inde 393 | 33 INDX OSX PHLX Oil Service Sector Inde 394 | 34 INDX XAL NYSE AMEX Airline Inde 395 | 35 INDX W5KSCG Wilshire US Small Cap Growt 396 | 36 INDX TWII Taiwan Weighted Inde 397 | 37 INDX ATX ATX Index (Austria 398 | 38 INDX NWX NYSE ARCA Networking Inde 399 | 39 INDX W5KSCV Wilshire US Small Cap Valu 400 | 40 INDX XAU PHLX Gold/Silver Sector Inde 401 | 41 INDX W5KMCV Wilshire US Mid Cap Valu 402 | 42 INDX WGREIT Wilshire Global REIT Inde 403 | 43 INDX SML S&P Small-Cap 600 Inde 404 | 44 INDX RUT Russell 2000 Inde 405 | 45 INDX JKSE Jakarta Composite Index (Indonesia 406 | 46 INDX BFX Euronext BEL-20 Index (Belgium) 407 | 47 INDX XBD NYSE AMEX Securities Broker/Dealer Inde 408 | 48 INDX RUA Russell 3000 Inde 409 | 49 INDX XII NYSE ARCA Institutional Inde 410 | 50 INDX IETP ISEQ 20 Price Index (Ireland 411 | 51 INDX DRG NYSE AMEX Pharmaceutical Inde 412 | 52 INDX W5000 Wilshire 5000 Total Market Inde 413 | 53 INDX HGX PHLX Housing Sector Inde 414 | 54 INDX MXX IPC Index (Mexico) 415 | 55 INDX W5KLCG Wilshire US Large Cap Growt 416 | 56 INDX STI Straits Times Index 417 | 57 INDX KS11 KOSPI Composite Index 418 | 58 INDX AEX AEX Amsterdam Index 419 | 59 INDX NYA NYSE Composite Index 420 | 60 INDX XMI NYSE ARCA Major Market Inde 421 | 61 INDX BTK NYSE AMEX Biotechnology Inde 422 | 62 INDX EPX NASDAQ SIG Oil Exploration and Production Inde 423 | 63 INDX MID S&P Mid-Cap 400 Inde 424 | 64 INDX HUI NYSE Arca Gold Bugs Inde 425 | 65 INDX SOX PHLX Semiconductor Inde 426 | 66 INDX HCX CBOE S&P Healthcare Index 427 | 67 INDX XCI NYSE AMEX Computer Technology Inde 428 | 68 INDX XNG NYSE AMEX Natural Gas Inde 429 | 69 INDX RMZ MSCI US REIT Inde 430 | 70 INDX WGRESI Wilshire Global Real Estate Securities Inde 431 | 71 INDX N225 Nikkei 225 Index (Japan 432 | 72 INDX VDAX Deutsche Boerse VDAX Volatility Inde 433 | 73 INDX MXY NYSE ARCA Mexico Inde 434 | 74 INDX OSEAX Oslo Exchange All Share Index (Norway) 435 | 75 INDX TYX Treasury Yield 30 Years Inde 436 | 76 INDX DJI Dow Jones Industrial Average 437 | 77 INDX AXPJ S&P/ASX 200 Australia REIT Inde 438 | 78 INDX PSI20 PSI 20 Stock Index (Portugal 439 | 79 INDX IRX 13-week Treasury Bill Inde 440 | 80 INDX FVX Treasury Yield 5 Years Inde 441 | 81 INDX NYI NYSE International 100 Index 442 | 82 INDX AXJO S&P/ASX 200 Index (Australia 443 | 83 INDX 512NTR S&P 500 GBP Hdg (Net TR) (^512NTR) 444 | 84 INDX CTES_VI Czech Trading Inde 445 | 85 INDX NSEI S&P/CNX Nifty Index (India 446 | 86 INDX NYY NYSE TMT Inde 447 | 87 INDX CCSI EGX 70 Price Index (Egypt 448 | 88 INDX SPSUPX S&P Composite 1500 Inde 449 | 89 INDX BVSP Bovespa Index (Brazil) 450 | 90 INDX ISEQ ISEQ Overall Price Index (Ireland 451 | 91 INDX JPN NYSE AMEX Japan Inde 452 | 92 INDX NYL NYSE World Leaders Inde 453 | 93 INDX TNX CBOE Interest Rate 10-Year T-Note Inde 454 | 94 INDX NY NYSE US 100 Inde 455 | 95 INDX SPLV PowerShares S&P 500 Low Volatil 456 | 96 INDX OMXSPI Stockholm General Index (Sweden) 457 | 97 INDX GVZ CBOE Gold Volatility Inde 458 | 98 INDX SPY SPDR S&P 500 (SPY 459 | 99 INDX IEQR_IR ISEQ General Total Return Index (Ireland 460 | 100 INDX OMXC20_CO OMX Copenhagen 20 Index 461 | 101 INDX DJUSFN ^DJUSFN: Dow Jones U.S. Financials Inde 462 | 102 INDX DJASD ^DJASD: Dow Jones Asia Select Dividen 463 | 103 INDX IMUS ^IMUS: Dow Jones Islamic Market U.S. 464 | 104 INDX W1SGI ^W1SGI: Dow Jones Sustainability Worl 465 | 105 INDX DJT ^DJT: Dow Jones Transportation Averag 466 | 106 INDX DJUSM ^DJUSM: Dow Jones U.S. Mid-Cap Inde 467 | 107 INDX W1XGA ^W1XGA: Dow Jones Sustainability Worl 468 | 108 INDX DWC ^DWC: DJUS Market Index (full-cap 469 | 109 INDX DJC ^DJC: Dow Jones-UBS Commodity Inde 470 | 110 INDX IMXL ^IMXL: Dow Jones Islamic Market Titan 471 | 111 INDX XLHK ^XLHK: Dow Jones Hong Kong Titans 30 472 | 112 INDX DJTMDI ^DJTMDI: Dow Jones Media Titans 30 Inde 473 | 113 INDX DJU ^DJU: Dow Jones Utility Averag 474 | 114 INDX DWCOGS ^DWCOGS: Dow Jones U.S. Oil & Gas Tota 475 | 115 INDX DJUSST ^DJUSST: Dow Jones U.S. Iron & Steel In 476 | 116 INDX PSE ^PSE: NYSE Arca Tech 100 Index - New York Stock Exchange 477 | 117 INDX DWCF ^DWCF: Dow Jones U.S. Total Stock Mar 478 | 118 INDX W1SUS ^W1SUS: Dow Jones Sustainability Worl 479 | 119 INDX DJASDT ^DJASDT: Dow Jones Asia Select Dividen 480 | 120 INDX RCI ^RCI: Dow Jones Composite All REIT I 481 | 121 INDX DJUSL ^DJUSL: Dow Jones U.S. Large-Cap Inde 482 | 122 INDX P1DOW ^P1DOW: Dow Jones Asia/Pacific Inde 483 | 123 INDX DJAT ^DJAT: Dow Jones Asian Titans 50 Inde 484 | 124 INDX DJUS ^DJUS: Dow Jones U.S. Inde 485 | 125 INDX DWMI ^DWMI: Dow Jones U.S. Micro-Cap Tota 486 | 126 INDX DJUSS ^DJUSS: Dow Jones U.S. Small-Cap Inde 487 | 127 INDX OMX OMXS 30 Index (Sweden 488 | 128 INDX STOXX50E EuroStoxx 50 Inde 489 | 129 INDX FTAS FTSE All-Share Index (UK) 490 | 130 INDX WIHUN_L FTSE HUngary Index 491 | 131 INDX WITUR_L FTSE Turkey Index 492 | 132 INDX WITHA_L FTSE Thailand Index 493 | 133 INDX WIPOL_L FTSE Poland Index 494 | 134 INDX WICZH_L FTSE Czech Republic Index 495 | 135 INDX OMXC20 OMX Copenhagen 20 Inde 496 | 136 INDX IXE ^IXE: Select Sector Spdr-energy Inde 497 | 137 INDX IXIC NASDAQ Composite 498 | 138 INDX SPEUP S&P EUROPE 350""" 499 | df: Union[pd.TextFileReader, pd.DataFrame] = pd.read_csv(StringIO(data), sep="\t") 500 | df: pd.DataFrame = df.set_index("ID") 501 | return df 502 | --------------------------------------------------------------------------------