├── docs ├── docs │ ├── help.md │ ├── contact.md │ ├── CNAME │ ├── img │ │ ├── full.png │ │ ├── logo-white.png │ │ ├── logo-purple.png │ │ └── stock-chart.png │ ├── guide │ │ ├── ticker │ │ │ ├── intro.md │ │ │ ├── options.md │ │ │ ├── historical.md │ │ │ └── miscellaneous.md │ │ ├── index.md │ │ ├── advanced.md │ │ ├── keyword_arguments.md │ │ └── research.md │ ├── css │ │ └── termynal.css │ ├── index.md │ ├── js │ │ ├── custom.js │ │ └── termynal.js │ └── release_notes.md └── mkdocs.yml ├── tests ├── __init__.py ├── test_screener.py ├── test_country.py ├── test_miscellaneous.py ├── test_browsers.py ├── test_research.py └── test_ticker.py ├── .gitattributes ├── CNAME ├── demo ├── demo.gif └── valuation_measures.PNG ├── .editorconfig ├── Makefile ├── .github ├── workflows │ ├── publish.yml │ ├── docs.yml │ └── build.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── yahooquery ├── __init__.py ├── screener.py ├── headless.py ├── session_management.py ├── misc.py ├── utils.py ├── research.py └── base.py ├── .pre-commit-config.yaml ├── LICENSE.txt ├── pyproject.toml ├── .gitignore ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md ├── README.md └── CHANGELOG.rst /docs/docs/help.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/docs/contact.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | yahooquery.dpguthrie.com 2 | -------------------------------------------------------------------------------- /docs/docs/CNAME: -------------------------------------------------------------------------------- 1 | yahooquery.dpguthrie.com -------------------------------------------------------------------------------- /demo/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dpguthrie/yahooquery/HEAD/demo/demo.gif -------------------------------------------------------------------------------- /docs/docs/img/full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dpguthrie/yahooquery/HEAD/docs/docs/img/full.png -------------------------------------------------------------------------------- /demo/valuation_measures.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dpguthrie/yahooquery/HEAD/demo/valuation_measures.PNG -------------------------------------------------------------------------------- /docs/docs/img/logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dpguthrie/yahooquery/HEAD/docs/docs/img/logo-white.png -------------------------------------------------------------------------------- /docs/docs/img/logo-purple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dpguthrie/yahooquery/HEAD/docs/docs/img/logo-purple.png -------------------------------------------------------------------------------- /docs/docs/img/stock-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dpguthrie/yahooquery/HEAD/docs/docs/img/stock-chart.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | end_of_line = lf 12 | 13 | [Makefile] 14 | indent_style = tab 15 | 16 | [*.md] 17 | trim_trailing_whitespace = false 18 | 19 | [*.{yml,toml,yaml}] 20 | indent_size = 2 -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PYTEST=uv run pytest 2 | 3 | install_dev: 4 | uv sync --all-extras 5 | 6 | install: 7 | uv sync 8 | 9 | lint: 10 | ruff check 11 | 12 | test: lint 13 | $(PYTEST) 14 | 15 | test_cov: 16 | # Run tests and prepare coverage report 17 | $(PYTEST) --cov=./ --cov-report=xml 18 | 19 | test_with_warnings: 20 | # Run tests and show warnings 21 | $(PYTEST) -W all -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | id-token: write 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Install uv 16 | uses: astral-sh/setup-uv@v3 17 | 18 | - name: Build and publish 19 | run: | 20 | uv build 21 | uv publish 22 | -------------------------------------------------------------------------------- /tests/test_screener.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from yahooquery import Screener 4 | 5 | 6 | def test_screener(): 7 | s = Screener() 8 | assert s.get_screeners("most_actives") is not None 9 | 10 | 11 | def test_available_screeners(): 12 | s = Screener() 13 | assert s.available_screeners is not None 14 | 15 | 16 | def test_bad_screener(): 17 | with pytest.raises(ValueError): 18 | s = Screener() 19 | assert s.get_screeners("most_active") 20 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | on: 3 | workflow_dispatch: 4 | 5 | jobs: 6 | deploy: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v4 11 | 12 | - name: Install uv 13 | uses: astral-sh/setup-uv@v5 14 | with: 15 | # Install a specific version of uv. 16 | version: "0.7.3" 17 | - run: uv pip install mkdocs-material "mkdocstrings[python]" mike 18 | - run: uv run mkdocs gh-deploy --force -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | branches: [master] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Install uv 13 | uses: astral-sh/setup-uv@v5 14 | with: 15 | version: "0.7.3" 16 | - name: Install dependencies 17 | run: uv sync --all-extras 18 | - name: Run tests and coverage 19 | run: uv run pytest tests/ -k "not test_research.py" 20 | - name: Upload coverage to Codecov 21 | uses: codecov/codecov-action@v2 22 | -------------------------------------------------------------------------------- /yahooquery/__init__.py: -------------------------------------------------------------------------------- 1 | """Python interface to unofficial Yahoo Finance API endpoints""" 2 | 3 | name = "yahooquery" 4 | __version__ = "2.4.1" 5 | 6 | 7 | from .misc import ( # noqa 8 | get_currencies, 9 | get_exchanges, 10 | get_market_summary, 11 | get_trending, 12 | search, 13 | ) 14 | from .research import Research # noqa 15 | from .screener import Screener # noqa 16 | from .ticker import Ticker # noqa 17 | 18 | __all__ = [ 19 | "Research", 20 | "Screener", 21 | "Ticker", 22 | "get_currencies", 23 | "get_exchanges", 24 | "get_market_summary", 25 | "get_trending", 26 | "search", 27 | ] 28 | -------------------------------------------------------------------------------- /tests/test_country.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from yahooquery import Ticker 4 | from yahooquery.constants import COUNTRIES 5 | 6 | TICKERS = [Ticker("aapl", country="brazil")] 7 | 8 | 9 | @pytest.fixture(params=TICKERS) 10 | def ticker(request): 11 | return request.param 12 | 13 | 14 | def test_country_change(ticker): 15 | ticker.country = "hong kong" 16 | assert ticker.country == "hong kong" 17 | 18 | 19 | def test_bad_country(): 20 | with pytest.raises(ValueError): 21 | assert Ticker("aapl", country="china") 22 | 23 | 24 | def test_default_query_param(ticker): 25 | assert ticker.default_query_params == COUNTRIES[ticker.country] 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /tests/test_miscellaneous.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from yahooquery import ( 4 | get_currencies, 5 | get_exchanges, 6 | get_market_summary, 7 | get_trending, 8 | search, 9 | ) 10 | 11 | 12 | def test_get_currencies(): 13 | assert get_currencies() is not None 14 | 15 | 16 | def test_get_exchanges(): 17 | assert get_exchanges() is not None 18 | 19 | 20 | def test_get_market_summary(): 21 | assert get_market_summary() is not None 22 | 23 | 24 | def test_get_trending(): 25 | assert get_trending() is not None 26 | 27 | 28 | def test_search(): 29 | assert search("aapl")["quotes"][0]["longname"] == "Apple Inc." 30 | 31 | 32 | def test_bad_get_trending(): 33 | with pytest.raises(KeyError): 34 | assert get_trending("zimbabwe") 35 | 36 | 37 | def test_bad_market_summary(): 38 | with pytest.raises(KeyError): 39 | assert get_market_summary("zimbabwe") 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_stages: [push] 2 | default_language_version: 3 | python: python3.9 4 | repos: 5 | - repo: local 6 | hooks: 7 | - repo: https://github.com/astral-sh/ruff-pre-commit 8 | rev: v0.11.9 9 | hooks: 10 | - id: ruff 11 | - id: ruff-format 12 | - repo: https://github.com/pre-commit/pre-commit-hooks 13 | rev: v2.1.0 14 | hooks: 15 | - id: trailing-whitespace 16 | stages: [commit,push] 17 | - id: check-added-large-files 18 | - id: check-ast 19 | stages: [commit,push] 20 | - id: check-case-conflict 21 | - id: check-byte-order-marker 22 | - id: check-executables-have-shebangs 23 | - id: check-docstring-first 24 | stages: [commit,push] 25 | - id: check-json 26 | - id: check-merge-conflict 27 | stages: [commit,push] 28 | - id: check-symlinks 29 | - id: check-vcs-permalinks 30 | - id: check-xml 31 | - id: check-yaml 32 | - id: debug-statements 33 | - id: detect-private-key 34 | - id: flake8 35 | stages: [commit,push] 36 | - id: forbid-new-submodules 37 | - id: no-commit-to-branch 38 | stages: [commit,push] 39 | args: 40 | - --branch=main -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Doug Guthrie 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. -------------------------------------------------------------------------------- /tests/test_browsers.py: -------------------------------------------------------------------------------- 1 | from curl_cffi import requests 2 | from curl_cffi.requests.exceptions import ImpersonateError 3 | 4 | from yahooquery.constants import BROWSERS 5 | 6 | 7 | def test_browser_headers(): 8 | """Test that each browser in BROWSERS dictionary can successfully make a request.""" 9 | test_url = "https://google.com" 10 | errors = [] 11 | 12 | for browser_name in BROWSERS.keys(): 13 | try: 14 | session = requests.Session( 15 | impersonate=browser_name, headers=BROWSERS[browser_name] 16 | ) 17 | response = session.get(test_url) 18 | if response.status_code != 200: 19 | errors.append( 20 | f"{browser_name}: Failed with status code {response.status_code}" 21 | ) 22 | except ImpersonateError as e: 23 | errors.append(f"{browser_name}: {str(e)}") 24 | except Exception as e: 25 | errors.append(f"{browser_name}: Unexpected error - {str(e)}") 26 | 27 | # Only fail if we collected any errors 28 | if errors: 29 | raise AssertionError("\n".join(errors)) 30 | -------------------------------------------------------------------------------- /docs/docs/guide/ticker/intro.md: -------------------------------------------------------------------------------- 1 | # Ticker - Intro 2 | 3 | ## Import Ticker 4 | 5 | ```python 6 | from yahooquery import Ticker 7 | ``` 8 | 9 | ## Create Instance 10 | 11 | To retrieve data from Yahoo Finance for a single stock, create an instance of the `Ticker` class by passing the company's ticker symbol as an argument: 12 | 13 | ```python 14 | aapl = Ticker('aapl') 15 | ``` 16 | 17 | Or, pass in multiple symbols to retrieve data for multiple companies. Symbols can be passed in as a list: 18 | 19 | ```python 20 | symbols = ['fb', 'aapl', 'amzn', 'nflx', 'goog'] 21 | tickers = Ticker(symbols) 22 | ``` 23 | 24 | They can also be passed in as a string: 25 | 26 | ```python 27 | symbols = 'fb aapl amzn nflx goog' 28 | 29 | tickers = Ticker(symbols) 30 | ``` 31 | 32 | !!! note 33 | Outside of a few properties / methods, each symbol will represent one request. 34 | 35 | For example: 36 | 37 | ```python 38 | from yahooquery import Ticker 39 | 40 | symbols = 'fb aapl amzn nflx goog' 41 | 42 | tickers = Ticker(symbols) 43 | 44 | # Retrieve each company's profile information 45 | data = tickers.asset_profile 46 | ``` 47 | 48 | By calling the `asset_profile` property on the `tickers` instance, we're making 5 separate calls to Yahoo Finance (determined by the number of symbols). 49 | -------------------------------------------------------------------------------- /docs/docs/guide/index.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | ## Classes 4 | 5 | The majority of the data available through the unofficial API can be obtained through the use of three classes: 6 | 7 | - [Ticker](ticker/intro.md) - Retrieve company-specific data 8 | - [Screener](screener.md) - Retrieve lists of stocks based on certain criteria 9 | - [Research](research.md) - Retrieve proprietary research reports and trade ideas (**REQUIRES YAHOO FINANCE PREMIUM SUBSCRIPTION**). 10 | 11 | Each class inherits functionality from a base class, `_YahooFinance`. As such, each class will accept the same [keyword arguments](keyword_arguments.md), which allows the user to make asynchronous requests, validate ticker symbols, retry failed requests, and much more. 12 | 13 | ## Functions 14 | 15 | The functions below allow for additional data retrieval: 16 | 17 | - [currency_converter](misc.md#currency_converter) - Retrieve current / historical conversion rate between two currencies 18 | - [get_currencies](misc.md#get_currencies) - Retrieve list of currencies 19 | - [get_exchanges](misc.md#get_exchanges) - Retrieve list of exchanges 20 | - [get_market_summary](misc.md#get_market_summary) - Retrieve summary data relevant to a country 21 | - [get_trending](misc.md#get_trending) - Retrieve trending securities relevant to a country 22 | - [search](misc.md#search) - Query Yahoo Finance for anything 23 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "yahooquery" 3 | version = "2.4.1" 4 | description = "Python wrapper for an unofficial Yahoo Finance API" 5 | authors = [ 6 | {name = "Doug Guthrie", email = "douglas.p.guthrie@gmail.com"} 7 | ] 8 | documentation = "https://yahooquery.dpguthrie.com" 9 | keywords = ["python", "API", "finance", "stocks", "Yahoo Finance"] 10 | license = {text = "MIT"} 11 | readme = "README.md" 12 | requires-python = ">=3.9,<4.0" 13 | dependencies = [ 14 | "curl-cffi>=0.10.0", 15 | "pandas>=2.2.0", 16 | "requests-futures>=1.0.1", 17 | "tqdm>=4.65.0", 18 | "lxml>=4.9.3", 19 | "beautifulsoup4>=4.12.2", 20 | ] 21 | 22 | [project.optional-dependencies] 23 | premium = ["selenium>=4.32.0"] 24 | dev = [ 25 | "pytest>=8.3.0", 26 | "ruff>=0.11.0", 27 | "pytest-cov>=6.1.0", 28 | "pre-commit>=4.2.0", 29 | "mkdocs-material>=9.6.0", 30 | ] 31 | 32 | [build-system] 33 | requires = ["hatchling"] 34 | build-backend = "hatchling.build" 35 | 36 | [tool.ruff] 37 | line-length = 88 38 | target-version = "py39" 39 | select = ["E", "F", "I", "N", "W", "B", "UP", "PL", "RUF"] 40 | ignore = [] 41 | 42 | [tool.ruff.isort] 43 | known-first-party = ["yahooquery"] 44 | import-heading-firstparty = "first party" 45 | import-heading-future = "future" 46 | import-heading-local = "local" 47 | import-heading-stdlib = "stdlib" 48 | import-heading-thirdparty = "third party" 49 | multi-line-output = 3 50 | include-trailing-comma = true 51 | force-grid-wrap = 0 52 | use-parentheses = true 53 | line-length = 88 54 | -------------------------------------------------------------------------------- /tests/test_research.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | from yahooquery import Research 6 | 7 | RESEARCH = [ 8 | Research(username=os.getenv("YF_USERNAME"), password=os.getenv("YF_PASSWORD")) 9 | ] 10 | 11 | 12 | @pytest.fixture(params=RESEARCH) 13 | def research(request): 14 | return request.param 15 | 16 | 17 | def test_reports(research): 18 | assert research.reports() is not None 19 | 20 | 21 | def test_reports_one_arg(research): 22 | assert research.reports(report_date="Last Week") is not None 23 | 24 | 25 | def test_reports_multiple_filters(research): 26 | assert ( 27 | research.reports( 28 | investment_rating="Bearish,Bullish", 29 | report_date="Last Week", 30 | sector=["Basic Materials", "Real Estate"], 31 | report_type="Analyst Report", 32 | ) 33 | is not None 34 | ) 35 | 36 | 37 | def test_reports_bad_arg(research): 38 | with pytest.raises(ValueError): 39 | assert research.reports(investment_type="Bearish") 40 | 41 | 42 | def test_reports_bad_option(research): 43 | with pytest.raises(ValueError): 44 | assert research.reports(report_type="Bad Report Type") 45 | 46 | 47 | def test_reports_bad_multiple(research): 48 | with pytest.raises(ValueError): 49 | assert research.reports(report_date=["Last Week", "Last Year"]) 50 | 51 | 52 | def test_trades(research): 53 | assert research.trades() is not None 54 | 55 | 56 | def test_trades_size(research): 57 | assert research.trades(300) is not None 58 | -------------------------------------------------------------------------------- /docs/docs/guide/advanced.md: -------------------------------------------------------------------------------- 1 | !!! info 2 | **For Yahoo Finance Premium Subscribers** 3 | 4 | There might be a use case for combining the functionalities of both the Ticker and Research class. And, ideally, the user wouldn't have to utilize the login functionality in both instances. Here's how you would do that: 5 | 6 | ```python 7 | from yahooquery import Research, Ticker 8 | 9 | r = Research(username='username@yahoo.com', password='password') 10 | 11 | # I want to retrieve last week's Bullish Analyst Report's 12 | # for the Financial Services sector 13 | df = r.reports( 14 | sector='Financial Services', 15 | report_date='Last Week', 16 | investment_rating='Bullish', 17 | report_type='Analyst Report' 18 | ) 19 | 20 | # But now I want to get the data I find relevant and run my own analysis 21 | 22 | # Using aapl as a default symbol (we will change that later). 23 | # But, the important part is passing the current session and crumb 24 | # from our Research instance 25 | tickers = Ticker('aapl', session=r.session, crumb=r.crumb) 26 | 27 | # Now, I can loop through the dataframe and retrieve relevant data for 28 | # each ticker within the dataframe utilizing the Ticker instance 29 | for i, row in df.iterrows(): 30 | tickers.symbols = row['Tickers'] 31 | data = tickers.p_company_360 32 | # Do something with data 33 | # ... 34 | 35 | # Or, pass all tickers to the Ticker instance 36 | ticker_list = df['Tickers'].tolist() 37 | ticker_list = list(set(flatten_list(ticker_list))) 38 | tickers = Ticker(ticker_list, session=r.session, crumb=r.crumb) 39 | data = tickers.p_company_360 40 | # Do something with data 41 | # ... 42 | ``` 43 | -------------------------------------------------------------------------------- /.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 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | .ruff_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # Jupyter Notebook 77 | .ipynb_checkpoints 78 | 79 | # IPython 80 | profile_default/ 81 | ipython_config.py 82 | 83 | # pyenv 84 | .python-version 85 | 86 | # celery beat schedule file 87 | celerybeat-schedule 88 | 89 | # SageMath parsed files 90 | *.sage.py 91 | 92 | # Environments 93 | .env 94 | .venv 95 | env/ 96 | venv/ 97 | ENV/ 98 | env.bak/ 99 | venv.bak/ 100 | 101 | # Spyder project settings 102 | .spyderproject 103 | .spyproject 104 | 105 | # Rope project settings 106 | .ropeproject 107 | 108 | # mkdocs documentation 109 | docs/site 110 | 111 | # mypy 112 | .mypy_cache/ 113 | .dmypy.json 114 | dmypy.json 115 | 116 | # Pyre type checker 117 | .pyre/ 118 | .vscode/ -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Yahooquery 2 | We would love your input and want to make contributing to this project as easy and transparent as possible, whether it's: 3 | 4 | - Reporting a bug 5 | - Discussing the current state of the code 6 | - Submitting a fix 7 | - Proposing new features 8 | - Becoming a maintainer 9 | 10 | ## We Develop with Github 11 | We use github to host code, to track issues and feature requests, as well as accept pull requests. 12 | 13 | ## We Use [Github Flow](https://guides.github.com/introduction/flow/index.html), So All Code Changes Happen Through Pull Requests 14 | Pull requests are the best way to propose changes to the codebase (we use [Github Flow](https://guides.github.com/introduction/flow/index.html)). We actively welcome your pull requests: 15 | 16 | 1. Fork the repo and create your branch from `master`. 17 | 2. If you've added code that should be tested, add tests. 18 | 3. If you've changed APIs, update the documentation. 19 | 4. Ensure the test suite passes. 20 | 5. Make sure your code lints. 21 | 6. Issue that pull request! 22 | 23 | ## Any contributions you make will be under the MIT Software License 24 | In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern. 25 | 26 | ## Report bugs using Github's [issues](https://github.com/dpguthrie/yahooquery/issues) 27 | We use GitHub issues to track public bugs. Report a bug by [opening a new issue](); it's that easy! 28 | 29 | ## Write bug reports with detail, background, and sample code 30 | [Here's a good example](http://stackoverflow.com/q/12488905/180626) of a bug report. 31 | 32 | **Great Bug Reports** tend to have: 33 | 34 | - A quick summary and/or background 35 | - Steps to reproduce 36 | - Be specific! 37 | - Give sample code if you can. 38 | - What you expected would happen 39 | - What actually happens 40 | - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) 41 | 42 | People *love* thorough bug reports. I'm not even kidding. 43 | 44 | ## License 45 | By contributing, you agree that your contributions will be licensed under its MIT License. 46 | -------------------------------------------------------------------------------- /docs/mkdocs.yml: -------------------------------------------------------------------------------- 1 | # Project Information 2 | site_name: yahooquery 3 | site_description: Python wrapper for unofficial Yahoo Finance API 4 | site_author: Doug Guthrie 5 | 6 | # Repository 7 | repo_name: yahooquery 8 | repo_url: https://github.com/dpguthrie/yahooquery 9 | 10 | # Configuration 11 | theme: 12 | name: material 13 | palette: 14 | primary: black 15 | icon: 16 | repo: fontawesome/brands/github-alt 17 | logo: img/logo-white.png 18 | favicon: img/logo-white.png 19 | language: en 20 | 21 | # Extras 22 | extra: 23 | social: 24 | - icon: fontawesome/brands/github-alt 25 | link: https://github.com/dpguthrie 26 | - icon: fontawesome/brands/linkedin 27 | link: https://www.linkedin.com/in/douglas-guthrie-07994a48/ 28 | - icon: fontawesome/brands/medium 29 | link: https://medium.com/@douglas.p.guthrie 30 | - icon: fontawesome/solid/globe 31 | link: https://dpguthrie.com 32 | 33 | extra_css: 34 | - css/termynal.css 35 | 36 | extra_javascript: 37 | - js/termynal.js 38 | - js/custom.js 39 | 40 | # Extensions 41 | markdown_extensions: 42 | - admonition 43 | - codehilite: 44 | guess_lang: false 45 | - toc: 46 | permalink: true 47 | - pymdownx.superfences 48 | - pymdownx.tabbed 49 | - pymdownx.details 50 | - pymdownx.emoji: 51 | emoji_index: !!python/name:materialx.emoji.twemoji 52 | emoji_generator: !!python/name:materialx.emoji.to_svg 53 | 54 | # Navigation 55 | nav: 56 | - index.md 57 | - User Guide: 58 | - guide/index.md 59 | - Classes: 60 | - guide/keyword_arguments.md 61 | - Ticker: 62 | - guide/ticker/intro.md 63 | - guide/ticker/modules.md 64 | - guide/ticker/financials.md 65 | - guide/ticker/options.md 66 | - guide/ticker/historical.md 67 | - guide/ticker/miscellaneous.md 68 | - guide/ticker/premium.md 69 | - guide/screener.md 70 | - guide/research.md 71 | - Advanced Configuration: guide/advanced.md 72 | - Functions: guide/misc.md 73 | - Alternatives: alternative.ipynb 74 | - Changelog: release_notes.md 75 | 76 | # Google Analytics 77 | google_analytics: 78 | - UA-174219875-1 79 | - auto 80 | 81 | # Plugins 82 | plugins: 83 | - search 84 | - mkdocs-jupyter 85 | -------------------------------------------------------------------------------- /docs/docs/css/termynal.css: -------------------------------------------------------------------------------- 1 | /** 2 | * termynal.js 3 | * 4 | * @author Ines Montani 5 | * @version 0.0.1 6 | * @license MIT 7 | */ 8 | 9 | :root { 10 | --color-bg: #252a33; 11 | --color-text: #eee; 12 | --color-text-subtle: #a2a2a2; 13 | } 14 | 15 | [data-termynal] { 16 | width: 750px; 17 | max-width: 100%; 18 | background: var(--color-bg); 19 | color: var(--color-text); 20 | font-size: 18px; 21 | font-family: 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; 22 | border-radius: 4px; 23 | padding: 75px 45px 35px; 24 | position: relative; 25 | -webkit-box-sizing: border-box; 26 | box-sizing: border-box; 27 | } 28 | 29 | [data-termynal]:before { 30 | content: ''; 31 | position: absolute; 32 | top: 15px; 33 | left: 15px; 34 | display: inline-block; 35 | width: 15px; 36 | height: 15px; 37 | border-radius: 50%; 38 | /* A little hack to display the window buttons in one pseudo element. */ 39 | background: #d9515d; 40 | -webkit-box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930; 41 | box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930; 42 | } 43 | 44 | [data-termynal]:after { 45 | content: 'bash'; 46 | position: absolute; 47 | color: var(--color-text-subtle); 48 | top: 5px; 49 | left: 0; 50 | width: 100%; 51 | text-align: center; 52 | } 53 | 54 | [data-ty] { 55 | display: block; 56 | line-height: 2; 57 | } 58 | 59 | [data-ty]:before { 60 | /* Set up defaults and ensure empty lines are displayed. */ 61 | content: ''; 62 | display: inline-block; 63 | vertical-align: middle; 64 | } 65 | 66 | [data-ty="input"]:before, 67 | [data-ty-prompt]:before { 68 | margin-right: 0.75em; 69 | color: var(--color-text-subtle); 70 | } 71 | 72 | [data-ty="input"]:before { 73 | content: '$'; 74 | } 75 | 76 | [data-ty][data-ty-prompt]:before { 77 | content: attr(data-ty-prompt); 78 | } 79 | 80 | [data-ty-cursor]:after { 81 | content: attr(data-ty-cursor); 82 | font-family: monospace; 83 | margin-left: 0.5em; 84 | -webkit-animation: blink 1s infinite; 85 | animation: blink 1s infinite; 86 | } 87 | 88 | 89 | /* Cursor animation */ 90 | 91 | @-webkit-keyframes blink { 92 | 50% { 93 | opacity: 0; 94 | } 95 | } 96 | 97 | @keyframes blink { 98 | 50% { 99 | opacity: 0; 100 | } 101 | } -------------------------------------------------------------------------------- /yahooquery/screener.py: -------------------------------------------------------------------------------- 1 | # stdlib 2 | import re 3 | from urllib import parse 4 | 5 | from yahooquery.base import _YahooFinance 6 | from yahooquery.constants import SCREENERS 7 | 8 | 9 | class Screener(_YahooFinance): 10 | def __init__(self, **kwargs): 11 | super().__init__(**kwargs) 12 | 13 | def _construct_params(self, config, params): 14 | new_params = {} 15 | optional_params = [ 16 | k 17 | for k in config["query"] 18 | if not config["query"][k]["required"] 19 | and config["query"][k]["default"] is not None 20 | ] 21 | for optional in optional_params: 22 | new_params.update( 23 | {optional: params.get(optional, config["query"][optional]["default"])} 24 | ) 25 | new_params.update(self.default_query_params) 26 | new_params = { 27 | k: str(v).lower() if v is True or v is False else v 28 | for k, v in new_params.items() 29 | } 30 | return [dict(new_params, scrIds=scrId) for scrId in params["scrIds"]] 31 | 32 | def _construct_urls(self, config, params): 33 | return [self.session.get(url=config["path"], params=p) for p in params] 34 | 35 | def _get_symbol(self, response, params, **kwargs): 36 | query_params = dict(parse.parse_qsl(parse.urlsplit(response.url).query)) 37 | screener_id = query_params["scrIds"] 38 | key = next(k for k in SCREENERS if SCREENERS[k]["id"] == screener_id) 39 | return key 40 | 41 | def _check_screen_ids(self, screen_ids): 42 | all_screeners = list(SCREENERS.keys()) 43 | if not isinstance(screen_ids, list): 44 | screen_ids = re.findall(r"[a-zA-Z0-9_]+", screen_ids) 45 | if any(elem not in all_screeners for elem in screen_ids): 46 | raise ValueError( 47 | "One of {} is not a valid screener. \ 48 | Please check available_screeners".format( 49 | ", ".join(screen_ids) 50 | ) 51 | ) 52 | return screen_ids 53 | 54 | @property 55 | def available_screeners(self): 56 | """Return list of keys available to pass to 57 | :func:`Screener.get_screeners` 58 | """ 59 | return list(SCREENERS.keys()) 60 | 61 | def get_screeners(self, screen_ids, count=25): 62 | """Return list of predefined screeners from Yahoo Finance 63 | 64 | Parameters: 65 | screen_ids (str or list): Keys corresponding to list 66 | screen_ids = 'most_actives day_gainers' 67 | screen_ids = ['most_actives', 'day_gainers'] 68 | count (int): Number of items to return, default=25 69 | """ 70 | valid_screen_ids = self._check_screen_ids(screen_ids) 71 | screen_ids = [SCREENERS[screener]["id"] for screener in valid_screen_ids] 72 | return self._get_data("screener", params={"scrIds": screen_ids, "count": count}) 73 | -------------------------------------------------------------------------------- /docs/docs/guide/ticker/options.md: -------------------------------------------------------------------------------- 1 | # Options 2 | 3 | ### **option_chain** 4 | 5 | === "Details" 6 | 7 | - *Description*: View option chain data for all expiration dates for a given symbol(s) 8 | - *Return*: `pandas.DataFrame` 9 | 10 | === "Example" 11 | 12 | ```python 13 | faang = Ticker('fb aapl amzn nflx goog') 14 | df = faang.option_chain 15 | df.head() 16 | ``` 17 | 18 | === "Data" 19 | 20 | | | contractSymbol | strike | currency | lastPrice | change | percentChange | volume | openInterest | bid | ask | contractSize | lastTradeDate | impliedVolatility | inTheMoney | 21 | |:----------------------------------------------------|:--------------------|---------:|:-----------|------------:|---------:|----------------:|---------:|---------------:|-------:|------:|:---------------|:--------------------|--------------------:|:-------------| 22 | | ('aapl', Timestamp('2020-07-31 00:00:00'), 'calls') | AAPL200731C00170000 | 170 | USD | 237.49 | 28.78 | 13.7895 | 1 | 4 | 239.8 | 244.1 | REGULAR | 2020-07-31 13:32:22 | 9.42383 | True | 23 | | ('aapl', Timestamp('2020-07-31 00:00:00'), 'calls') | AAPL200731C00175000 | 175 | USD | 206.7 | 0 | 0 | 1 | 1 | 235.15 | 239.1 | REGULAR | 2020-07-30 16:14:43 | 9.14454 | True | 24 | | ('aapl', Timestamp('2020-07-31 00:00:00'), 'calls') | AAPL200731C00180000 | 180 | USD | 191.6 | 0 | 0 | 0 | 1 | 229.8 | 234 | REGULAR | 2020-07-23 18:23:16 | 8.78321 | True | 25 | | ('aapl', Timestamp('2020-07-31 00:00:00'), 'calls') | AAPL200731C00185000 | 185 | USD | 188.38 | 0 | 0 | 1 | 0 | 224.9 | 229.1 | REGULAR | 2020-07-06 19:30:06 | 8.60938 | True | 26 | | ('aapl', Timestamp('2020-07-31 00:00:00'), 'calls') | AAPL200731C00190000 | 190 | USD | 173.7 | 0 | 0 | 0 | 1 | 178.4 | 182.9 | REGULAR | 2020-06-24 13:58:56 | 1e-05 | True | 27 | 28 | === "Filtering Examples" 29 | 30 | ```python 31 | faang = Ticker('fb aapl amzn nflx goog') 32 | df = faang.option_chain 33 | 34 | # The dataframe contains a MultiIndex 35 | df.index.names 36 | FrozenList(['symbol', 'expiration', 'optionType']) 37 | 38 | # Get specific expiration date for specified symbol 39 | df.loc['aapl', '2022-07-31'] 40 | 41 | # Get specific option type for expiration date for specified symbol 42 | df.loc['aapl', '2022-07-31', 'calls'] 43 | 44 | # Retrieve only calls for all symbols 45 | df.xs('calls', level=2) 46 | 47 | # Only include Apple in the money options 48 | df.loc[df['inTheMoney'] == True].xs('aapl') 49 | ``` -------------------------------------------------------------------------------- /yahooquery/headless.py: -------------------------------------------------------------------------------- 1 | # stdlib 2 | from typing import Dict, List 3 | 4 | # third party 5 | from requests.cookies import RequestsCookieJar 6 | 7 | try: 8 | # third party 9 | from selenium import webdriver 10 | from selenium.common.exceptions import TimeoutException 11 | from selenium.webdriver.chrome.service import Service 12 | from selenium.webdriver.common.by import By 13 | from selenium.webdriver.support import expected_conditions as EC 14 | from selenium.webdriver.support.ui import WebDriverWait 15 | except ImportError: 16 | # Selenium was not installed 17 | has_selenium = False 18 | else: 19 | has_selenium = True 20 | 21 | 22 | class YahooFinanceHeadless: 23 | LOGIN_URL = "https://login.yahoo.com" 24 | 25 | def __init__(self, username: str, password: str): 26 | self.username = username 27 | self.password = password 28 | self.cookies = RequestsCookieJar() 29 | chrome_options = webdriver.ChromeOptions() 30 | chrome_options.add_argument("--headless") 31 | chrome_options.add_argument("--no-sandbox") 32 | chrome_options.add_argument("--log-level=3") 33 | chrome_options.add_argument("--ignore-certificate-errors") 34 | chrome_options.add_argument("--ignore-ssl-errors") 35 | chrome_options.set_capability("pageLoadStrategy", "eager") 36 | service = Service() 37 | self.driver = webdriver.Chrome(service=service, options=chrome_options) 38 | 39 | def login(self): 40 | try: 41 | self.driver.execute_script(f"window.open('{self.LOGIN_URL}');") 42 | self.driver.switch_to.window(self.driver.window_handles[-1]) 43 | self.driver.find_element(By.ID, "login-username").send_keys(self.username) 44 | self.driver.find_element(By.XPATH, "//input[@id='login-signin']").click() 45 | password_element = WebDriverWait(self.driver, 10).until( 46 | EC.presence_of_element_located((By.ID, "login-passwd")) 47 | ) 48 | password_element.send_keys(self.password) 49 | self.driver.find_element(By.XPATH, "//button[@id='login-signin']").click() 50 | 51 | cookies = self.driver.get_cookies() 52 | self.driver.quit() 53 | self._add_cookies_to_jar(cookies) 54 | 55 | except TimeoutException: 56 | return ( 57 | "A timeout exception has occured. Most likely it's due " 58 | "to invalid login credentials. Please try again." 59 | ) 60 | 61 | def _add_cookies_to_jar(self, cookies: List[Dict]): 62 | for cookie in cookies: 63 | cookie_dict = { 64 | "name": cookie["name"], 65 | "value": cookie["value"], 66 | "domain": cookie["domain"], 67 | "path": cookie["path"], 68 | "expires": None, # You can set the expiration if available 69 | } 70 | self.cookies.set(**cookie_dict) 71 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at douglas.p.guthrie@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /yahooquery/session_management.py: -------------------------------------------------------------------------------- 1 | # stdlib 2 | import logging 3 | import random 4 | 5 | # third party 6 | from bs4 import BeautifulSoup 7 | from curl_cffi import requests 8 | from requests.exceptions import ConnectionError, RetryError, SSLError 9 | from requests_futures.sessions import FuturesSession 10 | 11 | # first party 12 | from yahooquery.constants import BROWSERS 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | DEFAULT_TIMEOUT = 5 18 | DEFAULT_SESSION_URL = "https://finance.yahoo.com" 19 | CRUMB_FAILURE = ( 20 | "Failed to obtain crumb. Ability to retrieve data will be significantly limited." 21 | ) 22 | DEFAULT_SETUP_RETRIES = 5 23 | 24 | 25 | def get_crumb(session): 26 | try: 27 | response = session.get("https://query2.finance.yahoo.com/v1/test/getcrumb") 28 | 29 | except (ConnectionError, RetryError): 30 | logger.critical(CRUMB_FAILURE) 31 | # Cookies most likely not set in previous request 32 | return None 33 | 34 | if isinstance(session, FuturesSession): 35 | crumb = response.result().text 36 | else: 37 | crumb = response.text 38 | 39 | if crumb is None or crumb == "" or "" in crumb: 40 | logger.critical(CRUMB_FAILURE) 41 | return None 42 | 43 | return crumb 44 | 45 | 46 | def setup_session(session: requests.Session, url: str = None): 47 | url = url or DEFAULT_SESSION_URL 48 | try: 49 | response = session.get(url, allow_redirects=True) 50 | except SSLError: 51 | counter = 0 52 | while counter < DEFAULT_SETUP_RETRIES: 53 | try: 54 | response = session.get(url, verify=False) 55 | break 56 | except SSLError: 57 | counter += 1 58 | 59 | if isinstance(session, FuturesSession): 60 | response = response.result() 61 | 62 | # check for and handle consent page:w 63 | if response.url.find("consent") >= 0: 64 | logger.debug(f'Redirected to consent page: "{response.url}"') 65 | 66 | soup = BeautifulSoup(response.content, "html.parser") 67 | 68 | params = {} 69 | for param in ["csrfToken", "sessionId"]: 70 | try: 71 | params[param] = soup.find("input", attrs={"name": param})["value"] 72 | except Exception as exc: 73 | logger.critical( 74 | f'Failed to find or extract "{param}" from response. Exception={exc}' 75 | ) 76 | return session 77 | 78 | logger.debug(f"params: {params}") 79 | 80 | response = session.post( 81 | "https://consent.yahoo.com/v2/collectConsent", 82 | data={ 83 | "agree": ["agree", "agree"], 84 | "consentUUID": "default", 85 | "sessionId": params["sessionId"], 86 | "csrfToken": params["csrfToken"], 87 | "originalDoneUrl": url, 88 | "namespace": "yahoo", 89 | }, 90 | ) 91 | 92 | return session 93 | 94 | 95 | def initialize_session(session=None, **kwargs): 96 | if session is None: 97 | max_workers = kwargs.pop("max_workers", 8) 98 | is_async = kwargs.pop("asynchronous", False) 99 | impersonate = random.choice(list(BROWSERS.keys())) 100 | headers = BROWSERS[impersonate] 101 | session = requests.Session(**kwargs, headers=headers, impersonate=impersonate) 102 | if is_async: 103 | session = FuturesSession(max_workers=max_workers, session=session) 104 | session = setup_session(session) 105 | return session 106 | -------------------------------------------------------------------------------- /docs/docs/guide/ticker/historical.md: -------------------------------------------------------------------------------- 1 | # Historical Prices 2 | 3 | ### **history** 4 | 5 | === "Details" 6 | 7 | - *Description*: Retreives historical pricing data (OHLC) for given symbol(s) 8 | - *Return*: `pandas.DataFrame` 9 | - *Arguments* 10 | 11 | | Argument | Description | Type | Default | Required | Options | 12 | |:-----------|:-----------------------------------------|:-----------------------------|:----------|:-----------|:--------------------------------------------------------------------------------------| 13 | | period | Length of time | `str` | `ytd` | optional | `['1d', '5d', '7d', '60d', '1mo', '3mo', '6mo', '1y', '2y', '5y', '10y', 'ytd', 'max']` | 14 | | interval | Time between data points | `str` | `1d` | optional | `['1m', '2m', '5m', '15m', '30m', '60m', '90m', '1h', '1d', '5d', '1wk', '1mo', '3mo']` | 15 | | start | Specific starting date to pull data from | `str` or `datetime.datetime` | | optional | If a string is passed, use the format `YYYY-MM-DD` | 16 | | end | Specific ending date | `str` or `datetime.datetime` | | optional | If a string is passed, use the format `YYYY-MM-DD` 17 | | adj_timezone | Adjust datetime to the specific symbol's timezone | `bool` | `True` | optional | `True`
`False` 18 | | adj_ohlc | Calculates an adjusted open, high, low and close prices according to split and dividend information | `bool` | `False` | optional | `True`
`False` 19 | 20 | !!! tip "One Minute Interval Data" 21 | The Yahoo Finance API restricts the amount of one minute interval data to seven days per request. However, the data availability extends to 30 days. The following will allow the user to retrieve the last 30 days of one minute interval data, with the one caveat that **4 requests are made in 7 day ranges to retrieve the desired data**: 22 | 23 | ```python 24 | tickers = Ticker('fb aapl nflx', asynchronous=True) 25 | 26 | df = tickers.history(period='1mo', interval='1m') 27 | ``` 28 | 29 | Thanks to [@rodrigobercini](https://github.com/rodrigobercini) for finding [this](https://github.com/dpguthrie/yahooquery/issues/32). 30 | 31 | 32 | === "Example" 33 | 34 | ```python 35 | tickers = Ticker('fb aapl nflx', asynchronous=True) 36 | 37 | # Default period = ytd, interval = 1d 38 | df = tickers.history() 39 | df.head() 40 | ``` 41 | 42 | === "Data" 43 | 44 | | | high | volume | close | low | open | adjclose | dividends | 45 | |:----------------------------------|-------:|------------:|--------:|-------:|-------:|-----------:|------------:| 46 | | ('fb', datetime.date(2020, 1, 2)) | 209.79 | 1.20771e+07 | 209.78 | 206.27 | 206.75 | 209.78 | 0 | 47 | | ('fb', datetime.date(2020, 1, 3)) | 210.4 | 1.11884e+07 | 208.67 | 206.95 | 207.21 | 208.67 | 0 | 48 | | ('fb', datetime.date(2020, 1, 6)) | 212.78 | 1.70589e+07 | 212.6 | 206.52 | 206.7 | 212.6 | 0 | 49 | | ('fb', datetime.date(2020, 1, 7)) | 214.58 | 1.49124e+07 | 213.06 | 211.75 | 212.82 | 213.06 | 0 | 50 | | ('fb', datetime.date(2020, 1, 8)) | 216.24 | 1.3475e+07 | 215.22 | 212.61 | 213 | 215.22 | 0 | 51 | 52 | ![Chart](../../img/stock-chart.png "YTD Daily Data") -------------------------------------------------------------------------------- /yahooquery/misc.py: -------------------------------------------------------------------------------- 1 | # stdlib 2 | 3 | # third party 4 | import pandas as pd 5 | 6 | from yahooquery.constants import COUNTRIES 7 | from yahooquery.session_management import initialize_session 8 | 9 | BASE_URL = "https://query2.finance.yahoo.com" 10 | 11 | 12 | def _make_request( 13 | url, response_field=None, country=None, method="get", params={}, data=None, **kwargs 14 | ): 15 | if country: 16 | country = country.lower() 17 | try: 18 | params.update(COUNTRIES[country]) 19 | except KeyError as err: 20 | raise KeyError( 21 | "{} is not a valid option. Valid options include {}".format( 22 | country, ", ".join(sorted(COUNTRIES.keys())) 23 | ) 24 | ) from err 25 | session = initialize_session(**kwargs) 26 | r = getattr(session, method)(url, params=params, json=data) 27 | json = r.json() 28 | if response_field: 29 | try: 30 | return json[response_field]["result"] 31 | except (TypeError, KeyError): 32 | return json[response_field] 33 | return json 34 | 35 | 36 | def search( 37 | query, 38 | country="United States", 39 | quotes_count=10, 40 | news_count=10, 41 | first_quote=False, 42 | ): 43 | """Search Yahoo Finance for anything 44 | 45 | Parameters 46 | ---------- 47 | query: str 48 | What to search for 49 | country: str, default 'united states', optional 50 | This allows you to alter the following query parameters that are 51 | sent with each request: lang, region, and corsDomain. 52 | quotes_count: int, default 10, optional 53 | Maximum amount of quotes to return 54 | news_count: int, default 0, optional 55 | Maximum amount of news items to return 56 | """ 57 | url = "https://query2.finance.yahoo.com/v1/finance/search" 58 | params = {"q": query, "quotes_count": quotes_count, "news_count": news_count} 59 | data = _make_request(url, country=country, params=params) 60 | if first_quote: 61 | return data["quotes"][0] if len(data["quotes"]) > 0 else data 62 | return data 63 | 64 | 65 | def get_currencies(): 66 | """Get a list of currencies""" 67 | url = f"{BASE_URL}/v1/finance/currencies" 68 | return _make_request(url, response_field="currencies", country="United States") 69 | 70 | 71 | def get_exchanges(): 72 | """Get a list of available exchanges and their suffixes""" 73 | url = "https://help.yahoo.com/kb/finance-for-web/SLN2310.html?impressions=true" 74 | dataframes = pd.read_html(url) 75 | return dataframes[0] 76 | 77 | 78 | def get_market_summary(country="United States"): 79 | """Get a market summary 80 | 81 | Parameters 82 | ---------- 83 | country: str, default 'united states', optional 84 | This allows you to alter the following query parameters that are 85 | sent with each request: lang, region, and corsDomain. 86 | 87 | Returns 88 | ------- 89 | 90 | """ 91 | url = f"{BASE_URL}/v6/finance/quote/marketSummary" 92 | return _make_request(url, response_field="marketSummaryResponse", country=country) 93 | 94 | 95 | def get_trending(country="United States"): 96 | """Get trending stocks for a specific region 97 | 98 | Parameters 99 | ---------- 100 | country: str, default 'united states', optional 101 | This allows you to alter the following query parameters that are 102 | sent with each request: lang, region, and corsDomain. 103 | """ 104 | try: 105 | region = COUNTRIES[country.lower()]["region"] 106 | except KeyError as err: 107 | raise KeyError( 108 | "{} is not a valid option. Valid options include {}".format( 109 | country, ", ".join(COUNTRIES.keys()) 110 | ) 111 | ) from err 112 | url = f"{BASE_URL}/v1/finance/trending/{region}" 113 | return _make_request(url, response_field="finance", country=country)[0] 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

5 | Python wrapper for an unofficial Yahoo Finance API 6 |

7 |

8 | 9 | Coverage 10 | 11 | 12 | Package version 13 | 14 | 15 | Downloads 16 | 17 |

18 | 19 | --- 20 | 21 | **Documentation**: https://yahooquery.dpguthrie.com 22 | 23 | **Interactive Demo**: https://yahooquery.streamlit.app/ 24 | 25 | **Source Code**: https://github.com/dpguthrie/yahooquery 26 | 27 | **Blog Post**: https://towardsdatascience.com/the-unofficial-yahoo-finance-api-32dcf5d53df 28 | 29 | --- 30 | 31 | ## Overview 32 | 33 | Yahooquery is a python interface to unofficial Yahoo Finance API endpoints. The package allows a user to retrieve nearly all the data visible via the Yahoo Finance front-end. 34 | 35 | Some features of yahooquery: 36 | 37 | - **Fast**: Data is retrieved through API endpoints instead of web scraping. Additionally, asynchronous requests can be utilized with simple configuration 38 | - **Simple**: Data for multiple symbols can be retrieved with simple one-liners 39 | - **User-friendly**: Pandas Dataframes are utilized where appropriate 40 | - **Premium**: Yahoo Finance premium subscribers are able to retrieve data available through their subscription 41 | 42 | ## Requirements 43 | 44 | Python 3.9+ - **Versions on or after 2.4.0 require python 3.9+** 45 | 46 | - [Pandas](https://pandas.pydata.org) - Fast, powerful, flexible and easy to use open source data analysis and manipulation tool 47 | - [Requests](https://requests.readthedocs.io/en/master/) - The elegant and simple HTTP library for Python, built for human beings. 48 | - [Requests-Futures](https://github.com/ross/requests-futures) - Asynchronous Python HTTP Requests for Humans 49 | 50 | ### Yahoo Finance Premium Subscribers 51 | 52 | - [Selenium](https://www.selenium.dev/selenium/docs/api/py/) - Web browser automation 53 | 54 | Selenium is only utilized to login to Yahoo, which is done when the user passes certain keyword arguments. Logging into Yahoo enables users who are subscribers to Yahoo Finance Premium to retrieve data only accessible to premium subscribers. 55 | 56 | ## Installation 57 | 58 | If you're a Yahoo Finance premium subscriber and would like to retrieve data available through your subscription, do the following: 59 | 60 | ```bash 61 | pip install yahooquery[premium] 62 | ``` 63 | 64 | Otherwise, omit the premium argument: 65 | 66 | ```bash 67 | pip install yahooquery 68 | ``` 69 | 70 | You can also install with uv if you have that installed: 71 | ```bash 72 | uv pip install yahooquery 73 | ``` 74 | 75 | ## Example 76 | 77 | The majority of the data available through the unofficial Yahoo Finance API is related to a company, which is represented in yahooquery as a `Ticker`. You can instantiate the `Ticker` class by passing the company's ticker symbol. For instance, to get data for Apple, Inc., pass `aapl` as the first argument to the `Ticker` class: 78 | 79 | ```python 80 | from yahooquery import Ticker 81 | 82 | aapl = Ticker('aapl') 83 | 84 | aapl.summary_detail 85 | ``` 86 | 87 | ## Multiple Symbol Example 88 | 89 | The `Ticker` class also makes it easy to retrieve data for a list of symbols with the same API. Simply pass a list of symbols as the argument to the `Ticker` class. 90 | 91 | ```python 92 | from yahooquery import Ticker 93 | 94 | symbols = ['fb', 'aapl', 'amzn', 'nflx', 'goog'] 95 | 96 | faang = Ticker(symbols) 97 | 98 | faang.summary_detail 99 | ``` 100 | 101 | ## License 102 | 103 | This project is licensed under the terms of the MIT license. 104 | -------------------------------------------------------------------------------- /yahooquery/utils.py: -------------------------------------------------------------------------------- 1 | # stdlib 2 | import datetime 3 | import re 4 | 5 | # third party 6 | import pandas as pd 7 | 8 | 9 | def flatten_list(ls): 10 | return [item for sublist in ls for item in sublist] 11 | 12 | 13 | def convert_to_list(symbols, comma_split=False): 14 | if isinstance(symbols, str): 15 | if comma_split: 16 | return [x.strip() for x in symbols.split(",")] 17 | else: 18 | return re.findall(r"[\w\-.=^&]+", symbols) 19 | return symbols 20 | 21 | 22 | def convert_to_timestamp(date=None, start=True): 23 | if date is not None: 24 | return int(pd.Timestamp(date).timestamp()) 25 | if start: 26 | return int(pd.Timestamp("1942-01-01").timestamp()) 27 | return int(pd.Timestamp.now().timestamp()) 28 | 29 | 30 | def _get_daily_index(data, index_utc, adj_timezone): 31 | # evalute if last indice represents a live interval 32 | timestamp = data["meta"]["regularMarketTime"] 33 | if timestamp is None: 34 | # without last trade data unable to ascertain if there's a live indice 35 | has_live_indice = False 36 | else: 37 | last_trade = pd.Timestamp.fromtimestamp(timestamp, tz="UTC") 38 | has_live_indice = index_utc[-1] >= last_trade - pd.Timedelta(2, "s") 39 | if has_live_indice: 40 | # remove it 41 | live_indice = index_utc[-1] 42 | index_utc = index_utc[:-1] 43 | one_day = datetime.timedelta(1) 44 | # evaluate if it should be put back later. If the close price for 45 | # the day is already included in the data, i.e. if the live indice 46 | # is simply duplicating data represented in the prior row, then the 47 | # following will evaluate to False (as live_indice will now be 48 | # within one day of the prior indice) 49 | keep_live_indice = index_utc.empty or live_indice > index_utc[-1] + one_day 50 | 51 | tz = data["meta"]["exchangeTimezoneName"] 52 | index_local = index_utc.tz_convert(tz) 53 | times = index_local.time 54 | 55 | bv = times <= datetime.time(14) 56 | if (bv).all() or data["meta"].get("exchangeName", "Nope") == "SAO": # see issue 163 57 | index = index_local.floor("d", ambiguous=True) 58 | elif (~bv).all(): 59 | index = index_local.ceil("d", ambiguous=True) 60 | else: 61 | # mix of open times pre and post 14:00. 62 | index1 = index_local[bv].floor("d", ambiguous=True) 63 | index2 = index_local[~bv].ceil("d", ambiguous=True) 64 | index = index1.union(index2) 65 | 66 | index = pd.Index(index.date) 67 | if has_live_indice and keep_live_indice: 68 | live_indice = live_indice.astimezone(tz) if adj_timezone else live_indice 69 | if not index.empty: 70 | index = index.insert(len(index), live_indice.to_pydatetime()) 71 | else: 72 | index = pd.Index([live_indice.to_pydatetime()], dtype="object") 73 | return index 74 | 75 | 76 | def _event_as_srs(event_data, event): 77 | index = pd.Index([int(v) for v in event_data.keys()], dtype="int64") 78 | if event == "dividends": 79 | values = [d["amount"] for d in event_data.values()] 80 | else: 81 | values = [ 82 | d["numerator"] / d["denominator"] if d["denominator"] else float("inf") 83 | for d in event_data.values() 84 | ] 85 | return pd.Series(values, index=index) 86 | 87 | 88 | def history_dataframe(data, daily, adj_timezone=True): 89 | data_dict = data["indicators"]["quote"][0].copy() 90 | cols = [ 91 | col for col in ("open", "high", "low", "close", "volume") if col in data_dict 92 | ] 93 | if "adjclose" in data["indicators"]: 94 | data_dict["adjclose"] = data["indicators"]["adjclose"][0]["adjclose"] 95 | cols.append("adjclose") 96 | 97 | if "events" in data: 98 | for event, event_data in data["events"].items(): 99 | if event not in ("dividends", "splits"): 100 | continue 101 | data_dict[event] = _event_as_srs(event_data, event) 102 | cols.append(event) 103 | 104 | df = pd.DataFrame(data_dict, index=data["timestamp"]) # align all on timestamps 105 | df.dropna(how="all", inplace=True) 106 | df = df[cols] # determine column order 107 | 108 | index = pd.to_datetime(df.index, unit="s", utc=True) 109 | if daily and not df.empty: 110 | index = _get_daily_index(data, index, adj_timezone) 111 | if len(index) == len(df) - 1: 112 | # a live_indice was removed 113 | df = df.iloc[:-1] 114 | elif adj_timezone: 115 | tz = data["meta"]["exchangeTimezoneName"] 116 | index = index.tz_convert(tz) 117 | 118 | df.index = index 119 | return df 120 | -------------------------------------------------------------------------------- /docs/docs/index.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

5 | Python wrapper for an unofficial Yahoo Finance API 6 |

7 |

8 | 9 | Coverage 10 | 11 | 12 | Package version 13 | 14 |

15 | 16 | --- 17 | 18 | **Documentation**: https://yahooquery.dpguthrie.com 19 | 20 | **Interactive Demo**: https://dpguthrie-yahooquery-streamlit-app-eydpjo.streamlit.app/ 21 | 22 | **Source Code**: https://github.com/dpguthrie/yahooquery 23 | 24 | **Blog Post**: https://towardsdatascience.com/the-unofficial-yahoo-finance-api-32dcf5d53df 25 | 26 | --- 27 | 28 | ## Overview 29 | 30 | Yahooquery is a python interface to unofficial Yahoo Finance API endpoints. The package allows a user to retrieve nearly all the data visible via the Yahoo Finance front-end. 31 | 32 | Some features of yahooquery: 33 | 34 | - **Fast**: Data is retrieved through API endpoints instead of web scraping. Additionally, asynchronous requests can be utilized with simple configuration 35 | - **Simple**: Data for multiple symbols can be retrieved with simple one-liners 36 | - **User-friendly**: Pandas Dataframes are utilized where appropriate 37 | - **Premium**: Yahoo Finance premium subscribers are able to retrieve data available through their subscription 38 | 39 | ## Requirements 40 | 41 | Python 2.7, 3.5+ 42 | 43 | - [Pandas](https://pandas.pydata.org) - Fast, powerful, flexible and easy to use open source data analysis and manipulation tool 44 | - [Requests](https://requests.readthedocs.io/en/master/) - The elegant and simple HTTP library for Python, built for human beings. 45 | - [Requests-Futures](https://github.com/ross/requests-futures) - Asynchronous Python HTTP Requests for Humans 46 | 47 | ### Yahoo Finance Premium Subscribers 48 | 49 | - [Selenium](https://www.selenium.dev/selenium/docs/api/py/) - Web browser automation 50 | 51 | !!! info 52 | Selenium is only utilized to login to Yahoo, which is done when the user passes certain [keyword arguments](guide/keyword_arguments.md#username-and-password). Logging into Yahoo enables users who are subscribers to Yahoo Finance Premium to retrieve data only accessible to premium subscribers. 53 | 54 | ## Installation 55 | 56 | If you're a Yahoo Finance premium subscriber and would like to retrieve data available through your subscription, do the following: 57 | 58 |
59 | pip install yahooquery[premium] 60 | 61 | Successfully installed yahooquery 62 | restart ↻ 63 |
64 | 65 | Otherwise, omit the premium argument: 66 | 67 |
68 | pip install yahooquery 69 | 70 | Successfully installed yahooquery 71 | restart ↻ 72 |
73 | 74 | Or install with `uv`: 75 | 76 |
77 | uv pip install yahooquery 78 | 79 | Successfully installed yahooquery 80 | restart ↻ 81 |
82 | 83 | ## Example 84 | 85 | The majority of the data available through the unofficial Yahoo Finance API is related to a company, which is represented in yahooquery as a `Ticker`. You can instantiate the `Ticker` class by passing the company's ticker symbol. For instance, to get data for :fontawesome-brands-apple:, pass `aapl` as the first argument to the `Ticker` class: 86 | 87 | ```python 88 | from yahooquery import Ticker 89 | 90 | aapl = Ticker('aapl') 91 | 92 | aapl.summary_detail 93 | ``` 94 | 95 | ## Multiple Symbol Example 96 | 97 | The `Ticker` class also makes it easy to retrieve data for a list of symbols with the same API. Simply pass a list of symbols as the argument to the `Ticker` class. 98 | 99 | ```python 100 | from yahooquery import Ticker 101 | 102 | symbols = ['fb', 'aapl', 'amzn', 'nflx', 'goog'] 103 | 104 | faang = Ticker(symbols) 105 | 106 | faang.summary_detail 107 | ``` 108 | 109 | ## License 110 | 111 | This project is licensed under the terms of the MIT license. 112 | -------------------------------------------------------------------------------- /docs/docs/js/custom.js: -------------------------------------------------------------------------------- 1 | const div = document.querySelector('.github-topic-projects') 2 | 3 | async function getDataBatch(page) { 4 | const response = await fetch(`https://api.github.com/search/repositories?q=topic:yahooquery&per_page=100&page=${page}`, { headers: { Accept: 'application/vnd.github.mercy-preview+json' } }) 5 | const data = await response.json() 6 | return data 7 | } 8 | 9 | async function getData() { 10 | let page = 1 11 | let data = [] 12 | let dataBatch = await getDataBatch(page) 13 | data = data.concat(dataBatch.items) 14 | const totalCount = dataBatch.total_count 15 | while (data.length < totalCount) { 16 | page += 1 17 | dataBatch = await getDataBatch(page) 18 | data = data.concat(dataBatch.items) 19 | } 20 | return data 21 | } 22 | 23 | function setupTermynal() { 24 | document.querySelectorAll(".termynal").forEach(node => { 25 | node.style.display = "block"; 26 | new Termynal(node, { 27 | lineDelay: 500 28 | }); 29 | }); 30 | const progressLiteralStart = "---> 100%"; 31 | const promptLiteralStart = "$ "; 32 | const customPromptLiteralStart = "# "; 33 | const termynalActivateClass = "termy"; 34 | let termynals = []; 35 | 36 | function createTermynals() { 37 | document 38 | .querySelectorAll(`.${termynalActivateClass} .highlight`) 39 | .forEach(node => { 40 | const text = node.textContent; 41 | const lines = text.split("\n"); 42 | const useLines = []; 43 | let buffer = []; 44 | function saveBuffer() { 45 | if (buffer.length) { 46 | let isBlankSpace = true; 47 | buffer.forEach(line => { 48 | if (line) { 49 | isBlankSpace = false; 50 | } 51 | }); 52 | dataValue = {}; 53 | if (isBlankSpace) { 54 | dataValue["delay"] = 0; 55 | } 56 | if (buffer[buffer.length - 1] === "") { 57 | // A last single
won't have effect 58 | // so put an additional one 59 | buffer.push(""); 60 | } 61 | const bufferValue = buffer.join("
"); 62 | dataValue["value"] = bufferValue; 63 | useLines.push(dataValue); 64 | buffer = []; 65 | } 66 | } 67 | for (let line of lines) { 68 | if (line === progressLiteralStart) { 69 | saveBuffer(); 70 | useLines.push({ 71 | type: "progress" 72 | }); 73 | } else if (line.startsWith(promptLiteralStart)) { 74 | saveBuffer(); 75 | const value = line.replace(promptLiteralStart, "").trimEnd(); 76 | useLines.push({ 77 | type: "input", 78 | value: value 79 | }); 80 | } else if (line.startsWith("// ")) { 81 | saveBuffer(); 82 | const value = "💬 " + line.replace("// ", "").trimEnd(); 83 | useLines.push({ 84 | value: value, 85 | class: "termynal-comment", 86 | delay: 0 87 | }); 88 | } else if (line.startsWith(customPromptLiteralStart)) { 89 | saveBuffer(); 90 | const promptStart = line.indexOf(promptLiteralStart); 91 | if (promptStart === -1) { 92 | console.error("Custom prompt found but no end delimiter", line) 93 | } 94 | const prompt = line.slice(0, promptStart).replace(customPromptLiteralStart, "") 95 | let value = line.slice(promptStart + promptLiteralStart.length); 96 | useLines.push({ 97 | type: "input", 98 | value: value, 99 | prompt: prompt 100 | }); 101 | } else { 102 | buffer.push(line); 103 | } 104 | } 105 | saveBuffer(); 106 | const div = document.createElement("div"); 107 | node.replaceWith(div); 108 | const termynal = new Termynal(div, { 109 | lineData: useLines, 110 | noInit: true, 111 | lineDelay: 500 112 | }); 113 | termynals.push(termynal); 114 | }); 115 | } 116 | 117 | function loadVisibleTermynals() { 118 | termynals = termynals.filter(termynal => { 119 | if (termynal.container.getBoundingClientRect().top - innerHeight <= 0) { 120 | termynal.init(); 121 | return false; 122 | } 123 | return true; 124 | }); 125 | } 126 | window.addEventListener("scroll", loadVisibleTermynals); 127 | createTermynals(); 128 | loadVisibleTermynals(); 129 | } 130 | 131 | async function main() { 132 | if (div) { 133 | data = await getData() 134 | div.innerHTML = '' 135 | const ul = document.querySelector('.github-topic-projects ul') 136 | data.forEach(v => { 137 | if (v.full_name === 'dpguthrie/yahooquery') { 138 | return 139 | } 140 | const li = document.createElement('li') 141 | li.innerHTML = `★ ${v.stargazers_count} - ${v.full_name} by @${v.owner.login}` 142 | ul.append(li) 143 | }) 144 | } 145 | 146 | setupTermynal(); 147 | } 148 | 149 | main() 150 | -------------------------------------------------------------------------------- /docs/docs/js/termynal.js: -------------------------------------------------------------------------------- 1 | /** 2 | * termynal.js 3 | * A lightweight, modern and extensible animated terminal window, using 4 | * async/await. 5 | * 6 | * @author Ines Montani 7 | * @version 0.0.1 8 | * @license MIT 9 | */ 10 | 11 | 'use strict'; 12 | 13 | /** Generate a terminal widget. */ 14 | class Termynal { 15 | /** 16 | * Construct the widget's settings. 17 | * @param {(string|Node)=} container - Query selector or container element. 18 | * @param {Object=} options - Custom settings. 19 | * @param {string} options.prefix - Prefix to use for data attributes. 20 | * @param {number} options.startDelay - Delay before animation, in ms. 21 | * @param {number} options.typeDelay - Delay between each typed character, in ms. 22 | * @param {number} options.lineDelay - Delay between each line, in ms. 23 | * @param {number} options.progressLength - Number of characters displayed as progress bar. 24 | * @param {string} options.progressChar – Character to use for progress bar, defaults to █. 25 | * @param {number} options.progressPercent - Max percent of progress. 26 | * @param {string} options.cursor – Character to use for cursor, defaults to ▋. 27 | * @param {Object[]} lineData - Dynamically loaded line data objects. 28 | * @param {boolean} options.noInit - Don't initialise the animation. 29 | */ 30 | constructor(container = '#termynal', options = {}) { 31 | this.container = (typeof container === 'string') ? document.querySelector(container) : container; 32 | this.pfx = `data-${options.prefix || 'ty'}`; 33 | this.originalStartDelay = this.startDelay = options.startDelay 34 | || parseFloat(this.container.getAttribute(`${this.pfx}-startDelay`)) || 600; 35 | this.originalTypeDelay = this.typeDelay = options.typeDelay 36 | || parseFloat(this.container.getAttribute(`${this.pfx}-typeDelay`)) || 90; 37 | this.originalLineDelay = this.lineDelay = options.lineDelay 38 | || parseFloat(this.container.getAttribute(`${this.pfx}-lineDelay`)) || 1500; 39 | this.progressLength = options.progressLength 40 | || parseFloat(this.container.getAttribute(`${this.pfx}-progressLength`)) || 40; 41 | this.progressChar = options.progressChar 42 | || this.container.getAttribute(`${this.pfx}-progressChar`) || '█'; 43 | this.progressPercent = options.progressPercent 44 | || parseFloat(this.container.getAttribute(`${this.pfx}-progressPercent`)) || 100; 45 | this.cursor = options.cursor 46 | || this.container.getAttribute(`${this.pfx}-cursor`) || '▋'; 47 | this.lineData = this.lineDataToElements(options.lineData || []); 48 | this.loadLines() 49 | if (!options.noInit) this.init() 50 | } 51 | 52 | loadLines() { 53 | // Load all the lines and create the container so that the size is fixed 54 | // Otherwise it would be changing and the user viewport would be constantly 55 | // moving as she/he scrolls 56 | const finish = this.generateFinish() 57 | finish.style.visibility = 'hidden' 58 | this.container.appendChild(finish) 59 | // Appends dynamically loaded lines to existing line elements. 60 | this.lines = [...this.container.querySelectorAll(`[${this.pfx}]`)].concat(this.lineData); 61 | for (let line of this.lines) { 62 | line.style.visibility = 'hidden' 63 | this.container.appendChild(line) 64 | } 65 | const restart = this.generateRestart() 66 | restart.style.visibility = 'hidden' 67 | this.container.appendChild(restart) 68 | this.container.setAttribute('data-termynal', ''); 69 | } 70 | 71 | /** 72 | * Initialise the widget, get lines, clear container and start animation. 73 | */ 74 | init() { 75 | /** 76 | * Calculates width and height of Termynal container. 77 | * If container is empty and lines are dynamically loaded, defaults to browser `auto` or CSS. 78 | */ 79 | const containerStyle = getComputedStyle(this.container); 80 | this.container.style.width = containerStyle.width !== '0px' ? 81 | containerStyle.width : undefined; 82 | this.container.style.minHeight = containerStyle.height !== '0px' ? 83 | containerStyle.height : undefined; 84 | 85 | this.container.setAttribute('data-termynal', ''); 86 | this.container.innerHTML = ''; 87 | for (let line of this.lines) { 88 | line.style.visibility = 'visible' 89 | } 90 | this.start(); 91 | } 92 | 93 | /** 94 | * Start the animation and rener the lines depending on their data attributes. 95 | */ 96 | async start() { 97 | this.addFinish() 98 | await this._wait(this.startDelay); 99 | 100 | for (let line of this.lines) { 101 | const type = line.getAttribute(this.pfx); 102 | const delay = line.getAttribute(`${this.pfx}-delay`) || this.lineDelay; 103 | 104 | if (type == 'input') { 105 | line.setAttribute(`${this.pfx}-cursor`, this.cursor); 106 | await this.type(line); 107 | await this._wait(delay); 108 | } 109 | 110 | else if (type == 'progress') { 111 | await this.progress(line); 112 | await this._wait(delay); 113 | } 114 | 115 | else { 116 | this.container.appendChild(line); 117 | await this._wait(delay); 118 | } 119 | 120 | line.removeAttribute(`${this.pfx}-cursor`); 121 | } 122 | this.addRestart() 123 | this.finishElement.style.visibility = 'hidden' 124 | this.lineDelay = this.originalLineDelay 125 | this.typeDelay = this.originalTypeDelay 126 | this.startDelay = this.originalStartDelay 127 | } 128 | 129 | generateRestart() { 130 | const restart = document.createElement('a') 131 | restart.onclick = (e) => { 132 | e.preventDefault() 133 | this.container.innerHTML = '' 134 | this.init() 135 | } 136 | restart.href = '#' 137 | restart.setAttribute('data-terminal-control', '') 138 | restart.innerHTML = "restart ↻" 139 | return restart 140 | } 141 | 142 | generateFinish() { 143 | const finish = document.createElement('a') 144 | finish.onclick = (e) => { 145 | e.preventDefault() 146 | this.lineDelay = 0 147 | this.typeDelay = 0 148 | this.startDelay = 0 149 | } 150 | finish.href = '#' 151 | finish.setAttribute('data-terminal-control', '') 152 | this.finishElement = finish 153 | return finish 154 | } 155 | 156 | addRestart() { 157 | const restart = this.generateRestart() 158 | this.container.appendChild(restart) 159 | } 160 | 161 | addFinish() { 162 | const finish = this.generateFinish() 163 | this.container.appendChild(finish) 164 | } 165 | 166 | /** 167 | * Animate a typed line. 168 | * @param {Node} line - The line element to render. 169 | */ 170 | async type(line) { 171 | const chars = [...line.textContent]; 172 | line.textContent = ''; 173 | this.container.appendChild(line); 174 | 175 | for (let char of chars) { 176 | const delay = line.getAttribute(`${this.pfx}-typeDelay`) || this.typeDelay; 177 | await this._wait(delay); 178 | line.textContent += char; 179 | } 180 | } 181 | 182 | /** 183 | * Animate a progress bar. 184 | * @param {Node} line - The line element to render. 185 | */ 186 | async progress(line) { 187 | const progressLength = line.getAttribute(`${this.pfx}-progressLength`) 188 | || this.progressLength; 189 | const progressChar = line.getAttribute(`${this.pfx}-progressChar`) 190 | || this.progressChar; 191 | const chars = progressChar.repeat(progressLength); 192 | const progressPercent = line.getAttribute(`${this.pfx}-progressPercent`) 193 | || this.progressPercent; 194 | line.textContent = ''; 195 | this.container.appendChild(line); 196 | 197 | for (let i = 1; i < chars.length + 1; i++) { 198 | await this._wait(this.typeDelay); 199 | const percent = Math.round(i / chars.length * 100); 200 | line.textContent = `${chars.slice(0, i)} ${percent}%`; 201 | if (percent>progressPercent) { 202 | break; 203 | } 204 | } 205 | } 206 | 207 | /** 208 | * Helper function for animation delays, called with `await`. 209 | * @param {number} time - Timeout, in ms. 210 | */ 211 | _wait(time) { 212 | return new Promise(resolve => setTimeout(resolve, time)); 213 | } 214 | 215 | /** 216 | * Converts line data objects into line elements. 217 | * 218 | * @param {Object[]} lineData - Dynamically loaded lines. 219 | * @param {Object} line - Line data object. 220 | * @returns {Element[]} - Array of line elements. 221 | */ 222 | lineDataToElements(lineData) { 223 | return lineData.map(line => { 224 | let div = document.createElement('div'); 225 | div.innerHTML = `${line.value || ''}`; 226 | 227 | return div.firstElementChild; 228 | }); 229 | } 230 | 231 | /** 232 | * Helper function for generating attributes string. 233 | * 234 | * @param {Object} line - Line data object. 235 | * @returns {string} - String of attributes. 236 | */ 237 | _attributes(line) { 238 | let attrs = ''; 239 | for (let prop in line) { 240 | // Custom add class 241 | if (prop === 'class') { 242 | attrs += ` class=${line[prop]} ` 243 | continue 244 | } 245 | if (prop === 'type') { 246 | attrs += `${this.pfx}="${line[prop]}" ` 247 | } else if (prop !== 'value') { 248 | attrs += `${this.pfx}-${prop}="${line[prop]}" ` 249 | } 250 | } 251 | 252 | return attrs; 253 | } 254 | } 255 | 256 | /** 257 | * HTML API: If current script has container(s) specified, initialise Termynal. 258 | */ 259 | if (document.currentScript.hasAttribute('data-termynal-container')) { 260 | const containers = document.currentScript.getAttribute('data-termynal-container'); 261 | containers.split('|') 262 | .forEach(container => new Termynal(container)) 263 | } 264 | -------------------------------------------------------------------------------- /yahooquery/research.py: -------------------------------------------------------------------------------- 1 | # stdlib 2 | import json 3 | from datetime import datetime, timedelta 4 | 5 | # third party 6 | import pandas as pd 7 | 8 | from yahooquery.base import _YahooFinance 9 | from yahooquery.utils import convert_to_list 10 | 11 | 12 | class Research(_YahooFinance): 13 | """Enable user interaction with research report and trade idea APIs 14 | 15 | Keyword Arguments: 16 | username (str): Yahoo username / email 17 | password (str): Yahoo password 18 | 19 | Note: 20 | The methods available through this class are only available for 21 | subscribers to Yahoo Finance Premium 22 | """ 23 | 24 | _OPERATORS = ["lt", "lte", "gt", "gte", "btwn", "eq", "and", "or"] 25 | 26 | _DATA = { 27 | "report": { 28 | "sortType": "DESC", 29 | "sortField": "report_date", 30 | "offset": 0, 31 | "size": 100, 32 | "entityIdType": "argus_reports", 33 | "includeFields": [ 34 | "report_date", 35 | "report_type", 36 | "report_title", 37 | "head_html", 38 | "ticker", 39 | "pdf_url", 40 | "snapshot_url", 41 | "sector", 42 | "id", 43 | "change_in_investment_rating", 44 | "investment_rating", 45 | "change_in_target_price", 46 | "change_in_earnings_per_share_estimate", 47 | ], 48 | }, 49 | "trade": { 50 | "sortType": "DESC", 51 | "sortField": "startdatetime", 52 | "offset": 0, 53 | "size": 100, 54 | "entityIdType": "trade_idea", 55 | "includeFields": [ 56 | "startdatetime", 57 | "term", 58 | "ticker", 59 | "rating", 60 | "price_target", 61 | "ror", 62 | "id", 63 | "image_url", 64 | "company_name", 65 | "price_timestamp", 66 | "current_price", 67 | "trade_idea_title", 68 | "highlights", 69 | "description", 70 | ], 71 | }, 72 | "earnings": { 73 | "sortType": "ASC", 74 | "sortField": "companyshortname", 75 | "offset": 0, 76 | "size": 100, 77 | "entityIdType": "earnings", 78 | "includeFields": [ 79 | "ticker", 80 | "companyshortname", 81 | "startdatetime", 82 | "startdatetimetype", 83 | "epsestimate", 84 | "epsactual", 85 | "epssurprisepct", 86 | ], 87 | }, 88 | "splits": { 89 | "sortType": "DESC", 90 | "sortField": "startdatetime", 91 | "entityIdType": "splits", 92 | "includeFields": [ 93 | "ticker", 94 | "companyshortname", 95 | "startdatetime", 96 | "optionable", 97 | "old_share_worth", 98 | "share_worth", 99 | ], 100 | }, 101 | "ipo": { 102 | "sortType": "DESC", 103 | "sortField": "startdatetime", 104 | "entityIdType": "ipo_info", 105 | "includeFields": [ 106 | "ticker", 107 | "companyshortname", 108 | "exchange_short_name", 109 | "filingdate", 110 | "startdatetime", 111 | "amendeddate", 112 | "pricefrom", 113 | "priceto", 114 | "offerprice", 115 | "currencyname", 116 | "shares", 117 | "dealtype", 118 | ], 119 | }, 120 | } 121 | 122 | TRENDS = {"options": ["Bearish", "Bullish"], "multiple": True} 123 | 124 | SECTORS = { 125 | "options": [ 126 | "Basic Materials", 127 | "Communication Services", 128 | "Consumer Cyclical", 129 | "Consumer Defensive", 130 | "Energy", 131 | "Financial Services", 132 | "Healthcare", 133 | "Industrial", 134 | "Real Estate", 135 | "Technology", 136 | "Utilities", 137 | ], 138 | "multiple": True, 139 | } 140 | 141 | REPORT_TYPES = { 142 | "options": [ 143 | "Analyst Report", 144 | "Insider Activity", 145 | "Market Outlook", 146 | "Market Summary", 147 | "Market Update", 148 | "Portfolio Ideas", 149 | "Quantitative Report", 150 | "Sector Watch", 151 | "Stock Picks", 152 | "Technical Analysis", 153 | "Thematic Portfolio", 154 | "Top/Bottom Insider Activity", 155 | ], 156 | "multiple": True, 157 | } 158 | 159 | DATES = { 160 | "options": {"Last Week": 7, "Last Month": 30, "Last Year": 365}, 161 | "multiple": False, 162 | } 163 | 164 | TERMS = { 165 | "field": "term", 166 | "options": ["Short term", "Mid term", "Long term"], 167 | "multiple": True, 168 | } 169 | 170 | _QUERY_OPTIONS = { 171 | "report": { 172 | "investment_rating": TRENDS, 173 | "sector": SECTORS, 174 | "report_type": REPORT_TYPES, 175 | "report_date": DATES, 176 | }, 177 | "trade": { 178 | "trend": TRENDS, 179 | "sector": SECTORS, 180 | "term": TERMS, 181 | "startdatetime": DATES, 182 | }, 183 | } 184 | 185 | def __init__(self, **kwargs): 186 | super().__init__(**kwargs) 187 | 188 | def _construct_date(self, n=0): 189 | return (datetime.now() - timedelta(days=n)).strftime("%Y-%m-%d") 190 | 191 | def _construct_query(self, research_type, **kwargs): 192 | operand_list = [] 193 | for k, v in kwargs.items(): 194 | v = convert_to_list(v, comma_split=True) 195 | if k not in self._QUERY_OPTIONS[research_type]: 196 | raise ValueError(f"{k} is an invalid argument for {research_type}") 197 | options = self._QUERY_OPTIONS[research_type][k]["options"] 198 | options = list(options.keys()) if isinstance(options, dict) else options 199 | if any(elem not in options for elem in v): 200 | raise ValueError( 201 | "{} is an invalid option for {}.".format(", ".join(v), k) 202 | ) 203 | if not self._QUERY_OPTIONS[research_type][k]["multiple"] and len(v) > 1: 204 | raise ValueError(f"Please provide only one value for {k}") 205 | operand_list.append(self._construct_operand(k, v, research_type)) 206 | if len(operand_list) == 0: 207 | return {} 208 | if len(operand_list) == 1: 209 | return operand_list[0] 210 | return {"operands": operand_list, "operator": "and"} 211 | 212 | def _construct_operand(self, k, v, research_type): 213 | if len(v) == 1: 214 | if isinstance(self._QUERY_OPTIONS[research_type][k]["options"], dict): 215 | days = self._QUERY_OPTIONS[research_type][k]["options"][v[0]] 216 | return { 217 | "operands": [k, self._construct_date(days), self._construct_date()], 218 | "operator": "btwn", 219 | } 220 | return {"operands": [k, v[0]], "operator": "eq"} 221 | else: 222 | d = {"operands": [], "operator": "or"} 223 | for elem in v: 224 | d["operands"].append({"operands": [k, elem], "operator": "eq"}) 225 | return d 226 | 227 | def _construct_urls(self, config, params, **kwargs): 228 | payloads = [ 229 | dict(kwargs.get("payload"), offset=i) 230 | for i in range(0, kwargs.get("size"), 100) 231 | ] 232 | return [ 233 | self.session.post(url=config["path"], params=params, json=payload) 234 | for payload in payloads 235 | ] 236 | 237 | def _get_symbol(self, response, params): 238 | body = response.request.body.decode("utf-8") 239 | return json.loads(body)["offset"] 240 | 241 | def _get_research(self, research_type, size, **kwargs): 242 | query = self._construct_query(research_type, **kwargs) 243 | payload = self._DATA[research_type] 244 | payload["query"] = query 245 | data = self._get_data("research", size=size, payload=payload) 246 | dataframes = [] 247 | try: 248 | for key in data.keys(): 249 | columns = [x["label"] for x in data[key]["documents"][0]["columns"]] 250 | dataframes.append( 251 | pd.DataFrame(data[key]["documents"][0]["rows"], columns=columns) 252 | ) 253 | return pd.concat(dataframes, ignore_index=True) 254 | except TypeError: 255 | return data 256 | 257 | def reports(self, size=100, **kwargs): 258 | """Retrieve research reports from Yahoo Finance 259 | 260 | Args: 261 | size (int, optional): Number of reports to return. Defaults to 100 262 | investment_rating (str or list, optional): Type of investment 263 | rating. See :py:attr:`~TRENDS` for available options 264 | sector (str or list, optional): Sector 265 | See :py:attr:`~SECTORS` for available options 266 | report_type (str or list, optional): Report types 267 | See :py:attr:`~REPORT_TYPES` for available options 268 | report_date (str, optional): Date range 269 | See :py:attr:`~DATES' for available options 270 | 271 | Returns: 272 | pandas.DataFrame: DataFrame consisting of research reports 273 | 274 | Raises: 275 | ValueError: If invalid keyword argument is passed, if invalid 276 | option is passed for keyword argument, or if multiple values 277 | are passed and only a single value is accepted 278 | """ 279 | return self._get_research("report", size, **kwargs) 280 | 281 | def trades(self, size=100, **kwargs): 282 | """Retrieve trade ideas from Yahoo Finance 283 | 284 | Args: 285 | size (int, optional): Number of trades to return. Defaults to 100 286 | trend (str or list, optional): Type of investment 287 | rating. See :py:attr:`~TRENDS` for available options 288 | sector (str or list, optional): Sector 289 | See :py:attr:`~SECTORS` for available options 290 | term (str or list, optional): Term length (short, mid, long) 291 | See :py:attr:`~TERMS` for available options 292 | startdatetime (str, optional): Date range 293 | See :py:attr:`~DATES' for available options 294 | 295 | Returns: 296 | pandas.DataFrame: DataFrame consisting of trade ideas 297 | 298 | Raises: 299 | ValueError: If invalid keyword argument is passed, if invalid 300 | option is passed for keyword argument, or if multiple values 301 | are passed and only a single value is accepted 302 | """ 303 | return self._get_research("trade", size, **kwargs) 304 | -------------------------------------------------------------------------------- /docs/docs/guide/keyword_arguments.md: -------------------------------------------------------------------------------- 1 | # Keyword Arguments 2 | 3 | ## Regular 4 | 5 | ### **asynchronous** 6 | 7 | === "Details" 8 | 9 | - *Description*: When set to `True`, requests made to Yahoo Finance will be made asynchronously 10 | - *Default*: `False` 11 | - *Type*: `bool` 12 | 13 | !!! tip 14 | Only necessary when you have more than one symbol 15 | 16 | === "Example" 17 | 18 | ```python hl_lines="4" 19 | symbols = 'fb aapl amzn nflx goog' 20 | Ticker( 21 | symbols, 22 | asynchronous=True 23 | ) 24 | ``` 25 | 26 | ### **backoff_factor** 27 | 28 | === "Details" 29 | 30 | - *Description*: A factor, in seconds, to apply between attempts after the second try 31 | - *Default*: `0.3` 32 | - *Implementation*: `{backoff_factor} * (2 ** ({number of total retries} - 1))` 33 | - *Example*: If the backoff factor is 0.1, then `sleep()` will sleep for [0.0s, 0.2s, 0.4s, ...] between retries 34 | 35 | === "Example" 36 | 37 | ```python hl_lines="3" 38 | Ticker( 39 | 'aapl', 40 | backoff_factor=1 41 | ) 42 | ``` 43 | 44 | ### **country** 45 | 46 | === "Details" 47 | 48 | - *Description*: Alter the language, region, and corsDomain that each request utilizes as a query parameter. 49 | - *Default*: `United States` 50 | 51 | !!! info 52 | This functionality has not been thoroughly tested as far as comparing data returned for each country. You will see a difference, though, in the data returned from the [news](ticker/miscellaneous.md#news) method: 53 | 54 | ??? example "View Countries" 55 | ```python 56 | { 57 | 'france': { 58 | 'lang': 'fr-FR', 59 | 'region': 'FR', 60 | 'corsDomain': 'fr.finance.yahoo.com' 61 | }, 62 | 'india': { 63 | 'lang': 'en-IN', 64 | 'region': 'IN', 65 | 'corsDomain': 'in.finance.yahoo.com' 66 | }, 67 | 'hong kong': { 68 | 'lang': 'zh-Hant-HK', 69 | 'region': 'HK', 70 | 'corsDomain': 'hk.finance.yahoo.com' 71 | }, 72 | 'germany': { 73 | 'lang': 'de-DE', 74 | 'region': 'DE', 75 | 'corsDomain': 'de.finance.yahoo.com' 76 | }, 77 | 'canada': { 78 | 'lang': 'en-CA', 79 | 'region': 'CA', 80 | 'corsDomain': 'ca.finance.yahoo.com' 81 | }, 82 | 'spain': { 83 | 'lang': 'es-ES', 84 | 'region': 'ES', 85 | 'corsDomain': 'es.finance.yahoo.com' 86 | }, 87 | 'italy': { 88 | 'lang': 'it-IT', 89 | 'region': 'IT', 90 | 'corsDomain': 'it.finance.yahoo.com' 91 | }, 92 | 'united states': { 93 | 'lang': 'en-US', 94 | 'region': 'US', 95 | 'corsDomain': 'finance.yahoo.com' 96 | }, 97 | 'australia': { 98 | 'lang': 'en-AU', 99 | 'region': 'AU', 100 | 'corsDomain': 'au.finance.yahoo.com' 101 | }, 102 | 'united kingdom': { 103 | 'lang': 'en-GB', 104 | 'region': 'GB', 105 | 'corsDomain': 'uk.finance.yahoo.com' 106 | }, 107 | 'brazil': { 108 | 'lang': 'pt-BR', 109 | 'region': 'BR', 110 | 'corsDomain': 'br.financas.yahoo.com' 111 | }, 112 | 'new zealand': { 113 | 'lang': 'en-NZ', 114 | 'region': 'NZ', 115 | 'corsDomain': 'nz.finance.yahoo.com' 116 | }, 117 | 'singapore': { 118 | 'lang': 'en-SG', 119 | 'region': 'SG', 120 | 'corsDomain': 'sg.finance.yahoo.com' 121 | }, 122 | 'taiwan': { 123 | 'lang': 'zh-tw', 124 | 'region': 'TW', 125 | 'corsDomain': 'tw.finance.yahoo.com' 126 | }, 127 | } 128 | ``` 129 | 130 | === "Example" 131 | 132 | ```python hl_lines="3" 133 | Ticker( 134 | 'aapl', 135 | country='France' 136 | ) 137 | ``` 138 | 139 | ### **formatted** 140 | 141 | === "Details" 142 | 143 | - *Description* - When `formatted=True`, most numerical data from the API will be returned as a dictionary: 144 | ```python 145 | "totalCash": { 146 | "raw": 94051000320 147 | "fmt": "94.05B" 148 | "longFmt": "94,051,000,320" 149 | } 150 | ``` 151 | When formatted is set to False, an internal method will return the value in the "raw" key. 152 | - *Default* - `False` 153 | - *Type* - `bool` 154 | 155 | !!! warning 156 | When `formatted=True`, all data will be returned as a `dict` 157 | 158 | === "Example" 159 | 160 | ```python hl_lines="3" 161 | Ticker( 162 | 'aapl', 163 | formatted=True 164 | ) 165 | ``` 166 | 167 | ### **max_workers** 168 | 169 | === "Details" 170 | 171 | - *Description* - Defines the number of workers used to make asynchronous requests. 172 | - *Default* - `8` 173 | - *Type* - `int` 174 | 175 | !!! tip 176 | This is only relevant when `asynchronous=True` 177 | 178 | === "Example" 179 | 180 | ```python hl_lines="3 4" 181 | Ticker( 182 | 'aapl', 183 | asynchronous=True, 184 | max_workers=4 185 | ) 186 | ``` 187 | 188 | ### **progress** 189 | 190 | === "Details" 191 | 192 | - *Description*: - Show a progress bar when downloading data 193 | - *Default* - `False` 194 | - *Type* - `bool` 195 | 196 | === "Example" 197 | 198 | ```python hl_lines="3" 199 | Ticker( 200 | 'aapl', 201 | progress=True 202 | ) 203 | ``` 204 | 205 | ### **proxies** 206 | 207 | === "Details" 208 | 209 | - *Description* - Make each request with a proxy. Simply pass a dictionary, mapping URL schemes to the URL to the proxy. 210 | - *Default* - `None` 211 | - *Type* - `dict` 212 | 213 | !!! tip 214 | You can also configure proxies by setting the environment variables `HTTP_PROXY` and `HTTPS_PROXY`. 215 | 216 | === "Example" 217 | 218 | ```python hl_lines="1 2 3 4 8" 219 | proxies = { 220 | 'http': 'http://10.10.1.10:3128', 221 | 'https': 'http://10.10.1.10:1080', 222 | } 223 | 224 | Ticker( 225 | 'aapl', 226 | proxies=proxies 227 | ) 228 | ``` 229 | 230 | ### **retry** 231 | 232 | === "Details" 233 | 234 | - *Description* - Number of times to retry a failed request 235 | - *Default* - `5` 236 | - *Type* - `int` 237 | 238 | === "Example" 239 | 240 | ```python hl_lines="3" 241 | Ticker( 242 | 'aapl', 243 | retry=10 244 | ) 245 | ``` 246 | 247 | ### **status_forcelist** 248 | 249 | === "Details" 250 | 251 | - *Description* - A set of integer HTTP status codes that we should force a retry on. 252 | - *Default* - `[429, 500, 502, 503, 504]` 253 | - *Type* - `list` 254 | 255 | !!! tip 256 | This is especially useful when retrieving historical pricing data for a large amount of symbols. Currently, Yahoo Finance has been displaying 404 errors for mass download requests. 257 | 258 | === "Example" 259 | 260 | ```python hl_lines="3" 261 | Ticker( 262 | 'aapl', 263 | status_forcelist=[404, 429, 500, 502, 503, 504] 264 | ) 265 | ``` 266 | 267 | ### **timeout** 268 | 269 | === "Details" 270 | 271 | - *Description* - Stop waiting for a response after a given number of seconds 272 | - *Default* - `5` 273 | - *Type* - `int` 274 | 275 | !!! note 276 | This is not a time limit on the entire response download; rather, an exception is raised if the server has not issued a response for timeout seconds (more precisely, if no bytes have been received on the underlying socket for timeout seconds). If no timeout is specified explicitly, requests do not time out. 277 | 278 | === "Example" 279 | 280 | ```python hl_lines="3" 281 | Ticker( 282 | 'aapl', 283 | timeout=3 284 | ) 285 | ``` 286 | 287 | ### **user_agent** 288 | 289 | === "Details" 290 | 291 | - *Description* - A browser's user-agent string that is sent with the headers with each request. 292 | - *Default* - Random selection from the list below: 293 | ```python 294 | USER_AGENT_LIST = [ 295 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36', 296 | 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36', 297 | 'Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36', 298 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36', 299 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36', 300 | 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36', 301 | ] 302 | ``` 303 | - *Type* - `str` 304 | 305 | === "Example" 306 | 307 | ```python hl_lines="3" 308 | Ticker( 309 | 'aapl', 310 | user_agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36" 311 | ) 312 | ``` 313 | 314 | ### **validate** 315 | 316 | === "Details" 317 | 318 | - *Description* - Validate existence of symbols during instantiation. Invalid symbols will be dropped but you can view them through the `invalid_symbols` property. 319 | - *Default* - `False` 320 | - *Type* - `bool` 321 | 322 | === "Example" 323 | 324 | ```python hl_lines="7 10" 325 | from yahooquery import Ticker 326 | 327 | symbols = 'fb facebook aapl apple amazon amzn netflix nflx goog alphabet' 328 | 329 | t = Ticker( 330 | symbols, 331 | validate=True) 332 | print(t.symbols) 333 | ['NFLX', 'GOOG', 'AAPL', 'FB', 'AMZN'] 334 | print(t.invalid_symbols) 335 | ['FACEBOOK', 'AMAZON', 'APPLE', 'NETFLIX', 'ALPHABET'] 336 | ``` 337 | 338 | ### **verify** 339 | 340 | === "Details" 341 | 342 | - *Description* - Either a boolean, in which case it controls whether we verify the server’s TLS certificate, or a string, in which case it must be a path to a CA bundle to use. 343 | - *Default* - `True` 344 | - *Type* - `bool` or `str` 345 | 346 | === "Example" 347 | 348 | ```python hl_lines="3" 349 | Ticker( 350 | 'aapl', 351 | verify=False 352 | ) 353 | ``` 354 | 355 | ## Premium 356 | 357 | ### **username and password** 358 | 359 | === "Details" 360 | 361 | - *Description*: If you're a subscriber to Yahoo Finance Premium, you'll be able to retrieve data available through that subscription. Simply pass your `username` and `password` to the `Ticker` class 362 | 363 | !!! note 364 | Selenium is utilized to login to Yahoo. It should take around 15-20 seconds to login. 365 | 366 | !!! tip 367 | Set environment variables for your username and password as `YF_USERNAME` and `YF_PASSWORD`, respectively. 368 | 369 | === "Example" 370 | 371 | ```python hl_lines="3 4" 372 | Ticker( 373 | 'aapl', 374 | username='fake_username', 375 | password='fake_password' 376 | ) 377 | ``` 378 | 379 | ## Advanced 380 | 381 | ### **crumb and session** 382 | 383 | === "Details" 384 | 385 | - *Description*: Some requests to Yahoo Finance require a crumb to make the request. **This is only utilized for [advanced configuration](advanced.md)** 386 | - *Default*: `None` 387 | - *Type*: `str` 388 | 389 | === "Example" 390 | 391 | See the [Advanced Section](advanced.md) 392 | -------------------------------------------------------------------------------- /docs/docs/guide/research.md: -------------------------------------------------------------------------------- 1 | 2 | The Research class is the access point to retrieve either research reports or trade ideas from Yahoo Finance. **You must be a subscriber to Yahoo Finance Premium to utilize this class.** 3 | 4 | ## Import 5 | 6 | ```python 7 | from yahooquery import Research 8 | ``` 9 | 10 | ## Create Instance 11 | 12 | ```python 13 | r = Research(username='username@yahoo.com', password='password') 14 | ``` 15 | 16 | ## Research 17 | 18 | ### **reports** 19 | 20 | === "Details" 21 | 22 | - *Description*: Retrieve research reports from Yahoo Finance 23 | - *Return*: `pandas.DataFrame` 24 | - *Arguments* 25 | 26 | | Argument | Description | Type | Default | Required | Options | 27 | |:-----------|:-----------|:-------|:----------|:-----------|:------------------------------| 28 | | size | Number of trades to return | `int` | `100` | optional | | 29 | | investment_rating | Type of investment rating |`str` or `list` | None | optional | See below | 30 | | sector | Sector | `str` or `list` | None | optional | See below | 31 | | report_type | Report types | `str` or `list` | None | optional | See below | 32 | | report_date | Date range | `str` | None | optional | See below | 33 | 34 | === "investment_rating" 35 | 36 | ```python 37 | { 38 | 'options': ['Bearish', 'Bullish'], 39 | 'multiple': True 40 | } 41 | ``` 42 | 43 | === "sector" 44 | 45 | ```python 46 | { 47 | 'options': [ 48 | 'Basic Materials', 'Communication Services', 'Consumer Cyclical', 49 | 'Consumer Defensive', 'Energy', 'Financial Services', 'Healthcare', 50 | 'Industrial', 'Real Estate', 'Technology', 'Utilities'], 51 | 'multiple': True 52 | } 53 | ``` 54 | 55 | === "report_type" 56 | 57 | ```python 58 | { 59 | 'options': [ 60 | 'Analyst Report', 'Insider Activity', 'Market Outlook', 'Market Summary', 61 | 'Market Update', 'Portfolio Ideas', 'Quantitative Report', 'Sector Watch', 62 | 'Stock Picks', 'Technical Analysis', 'Thematic Portfolio', 'Top/Bottom Insider Activity' 63 | ], 64 | 'multiple': True 65 | } 66 | ``` 67 | 68 | === "report_date" 69 | 70 | ```python 71 | { 72 | 'options': { 73 | 'Last Week': 7, 74 | 'Last Month': 30, 75 | 'Last Year': 365 76 | }, 77 | 'multiple': False 78 | } 79 | ``` 80 | 81 | !!! warning 82 | If using a `str` for the arguments that can either be a `str` or `list`, they have to be comma separated, i.e. `sector='Financial Services, Technology'`. 83 | 84 | === "Example" 85 | 86 | ```python hl_lines="2 3 4 5" 87 | r = Research(username='username@yahoo.com', password=password) 88 | r.reports( 89 | report_type='Analyst Report, Insider Activity', 90 | report_date='Last Week' 91 | ) 92 | ``` 93 | 94 | === "Data" 95 | 96 | | | Report Date | Report type | Report title | Tickers | Sector | Rating | Investment Rating | Target Price | Earnings Estimates | 97 | |---:|:-------------------------|:---------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------|:-----------------------|:-----------|:--------------------|:---------------|:---------------------| 98 | | 0 | 2020-07-31T00:00:00.000Z | Analyst Report | CME Group is a futures and derivatives exchange and clearing company. It operates exchanges such as the Chicago Mercantile Exchange (CME), Chicago Board of Trade (CBOT), New York Mercantile Exchange (NYMEX), Commodity Exchange (COMEX) and the Kansas City Board of Trade (KCBT). In addition, CME offers a range of market data and information services. CME shares are a component of the S&P 500. | ['CME'] | ['Financial Services'] | Maintained | Bullish | Decreased | Decreased | 99 | | 1 | 2020-07-31T00:00:00.000Z | Analyst Report | Mastercard operates the world's second-largest electronic payments network, providing processing services and payment product platforms, including credit, debit, ATM, prepaid and commercial payments under the Mastercard, Maestro, and Cirrus brands. Mastercard went public in 2006 and is a member of the S&P 500. | ['MA'] | ['Financial Services'] | Maintained | Bullish | Increased | Decreased | 100 | | 2 | 2020-07-31T00:00:00.000Z | Analyst Report | Blackstone Group is one of the world's leading managers of alternative assets, including private equity, real estate, hedge funds, credit-oriented funds, and closed-end mutual funds. In recent years, Blackstone has rapidly grown its fee-earning assets under management, and its assets are relatively well balanced among private equity, real estate, hedge funds, and credit. The company converted from a publicly traded partnership to a corporation on July 1, 2019. | ['BX'] | ['Financial Services'] | Maintained | Bullish | Maintained | Decreased | 101 | | 3 | 2020-07-31T00:00:00.000Z | Analyst Report | Northrop Grumman is a leading global defense contractor, providing systems integration, defense electronics, information technology, and advanced aircraft and space technology. The shares are a component of the S&P 500. The company has 90,000 employees. | ['NOC'] | ['Industrials'] | Maintained | Bullish | Maintained | Increased | 102 | | 4 | 2020-07-31T00:00:00.000Z | Analyst Report | Starbucks is a leading retailer of fresh-brewed coffee and branded merchandise. Its brands include Starbucks, Tazo Tea, and Frappuccino. With a market cap of more than $90 billion, SBUX shares are generally considered large-cap growth. | ['SBUX'] | ['Consumer Cyclical'] | Maintained | Bullish | Maintained | Decreased | 103 | 104 | 105 | ### **trades** 106 | 107 | === "Details" 108 | 109 | - *Description*: Retrieve trade ideas from Yahoo Finance 110 | - *Return*: `pandas.DataFrame` 111 | - *Arguments* 112 | 113 | | Argument | Description | Type | Default | Required | Options | 114 | |:-----------|:-----------|:-------|:----------|:-----------|:------------------------------| 115 | | size | Number of trades to return | `int` | `100` | optional | | 116 | | trend | Type of investment rating |`str` or `list` | None | optional | See below | 117 | | sector | Sector | `str` or `list` | None | optional | See below | 118 | | term | Term length (short, mid, long) | `str` or `list` | None | optional | See below | 119 | | startdatetime | Date range | `str` | None | optional | See below | 120 | 121 | === "trend" 122 | 123 | ```python 124 | { 125 | 'options': ['Bearish', 'Bullish'], 126 | 'multiple': True 127 | } 128 | ``` 129 | 130 | === "sector" 131 | 132 | ```python 133 | { 134 | 'options': [ 135 | 'Basic Materials', 'Communication Services', 'Consumer Cyclical', 136 | 'Consumer Defensive', 'Energy', 'Financial Services', 'Healthcare', 137 | 'Industrial', 'Real Estate', 'Technology', 'Utilities'], 138 | 'multiple': True 139 | } 140 | ``` 141 | 142 | === "term" 143 | 144 | ```python 145 | { 146 | 'options': ['Short term', 'Mid term', 'Long term'], 147 | 'multiple': True 148 | } 149 | ``` 150 | 151 | === "startdatetime" 152 | 153 | ```python 154 | { 155 | 'options': { 156 | 'Last Week': 7, 157 | 'Last Month': 30, 158 | 'Last Year': 365 159 | }, 160 | 'multiple': False 161 | } 162 | ``` 163 | 164 | !!! warning 165 | If using a `str` for the arguments that can either be a `str` or `list`, they have to be comma separated, i.e. `sector='Financial Services, Technology'`. 166 | 167 | 168 | === "Example" 169 | 170 | ```python hl_lines="2 3 4 5 6" 171 | r = Research(username='username@yahoo.com', password=password) 172 | r.trades( 173 | sector=['Financial Services', 'Technology'], 174 | term='Short term', 175 | startdatetime='Last Week' 176 | ) 177 | ``` 178 | 179 | === "Data" 180 | 181 | | | Idea Date | Term | Ticker | Rating | Price Target | Rate of Return | ID | Image URL | Company Name | Price Timestamp | Current Price | Title | Highlights | Description | 182 | |---:|:-------------------------|:-----------|:---------|:---------|---------------:|-----------------:|:--------------------------------|:-------------------------------------------------------------|:------------------------------|------------------:|----------------:|:----------------------------------------------------------------------------------|:-------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 183 | | 0 | 2020-07-30T00:00:00.000Z | Short term | BHLB | Sell | 8.3 | -0.177134 | tc_USvyP6AAPGJwApgABAACAAAD6CIg | https://s.yimg.com/uc/fin/img/bearish-continuation-wedge.svg | Berkshire Hills Bancorp, Inc. | 1596225965000 | 9.96 | Berkshire Hills Bancorp, Inc. - BHLB forms a Continuation Wedge (Bearish) pattern | | ['This stock has formed a pattern called Continuation Wedge (Bearish), providing a target price for the short-term in the range of 8.10 to 8.50.', 'The price recently crossed below its moving average signaling a new downtrend has been established.'] | 184 | | 1 | 2020-07-30T00:00:00.000Z | Short term | FISV | Buy | 107.82 | 0.070492 | tc_USvyO_AAPKkQAGgABAACAAAD6CJg | https://s.yimg.com/uc/fin/img/bullish-double-bottom.svg | Fiserv, Inc. | 1596225601000 | 99.79 | Fiserv, Inc. - FISV forms a Double Bottom pattern | | ['This stock has formed a pattern called Double Bottom, providing a target price for the short-term in the range of 107.00 to 108.50.', 'The Intermediate-Term KST indicator has triggered a bullish signal by rising above its moving average.'] | 185 | 186 | -------------------------------------------------------------------------------- /docs/docs/release_notes.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | 2.4.0 4 | ----- 5 | ## Update 6 | - Update to use uv for packaging and publishing 7 | - Folder, file structure 8 | - `validate_symbols` method now returns a tuple of valid and invalid symbols 9 | 10 | ## Add 11 | - `curl_cffi` to dependencies instead of `requests` for session management 12 | 13 | 2.3.7 14 | ----- 15 | ## Add 16 | - Logic for handling setting up a session when a consent screen is encountered. This is primarily seen in European countries 17 | and should allow for the continued use of this package. 18 | - Keyword argument, `setup_url`, to the base `_YahooFinance` class that allows a user to override the url used in setting up the session. As a default 19 | the Yahoo Finance home page is used (https://finance.yahoo.com). You can also create an environment variable, `YF_SETUP_URL` that will be used if set. 20 | Example usage: 21 | ```python 22 | import yahooquery as yq 23 | 24 | t = yq.Ticker('aapl', setup_url='https://finance.yahoo.com/quote/AAPL') 25 | ``` 26 | 27 | ## Remove 28 | - Webdriver manager is no longer used internally. Selenium Manager is now fully included with selenium `4.10.0`, so this package is no longer needed. 29 | 30 | 2.3.6 31 | ----- 32 | ## Fix 33 | - Use the previously instantiated session within the `Ticker` class (that may also contain proxies, other important session info) to retrieve both cookies and a crumb 34 | 35 | 2.3.5 36 | ----- 37 | ## Fix 38 | - Fix bad assignment to crumb when using username/password for premium access 39 | 40 | 2.3.4 41 | ----- 42 | ## Update 43 | - Use a different url to try and obtain cookies (fc.yahoo.com no longer works) 44 | - Refactor how a session is initialized 45 | - Use the country as a way to make the request to obtain cookies be more location specific 46 | 47 | 2.3.3 48 | ----- 49 | ## Update 50 | - Try and obtain cookies and a crumb during `Ticker` initialization so they can be used in further requests. 51 | 52 | 2.3.2 53 | ----- 54 | ## Update 55 | - Update quote summary endpoint from v10 to v6. The v10 endpoint currently requires a crumb as a query parameter, which is not something this library does not currently support. 56 | 57 | ## Fix 58 | - Bug related to retrieving screen IDs with a number 59 | 60 | 2.3.1 61 | ----- 62 | ## Fix 63 | - Fixes for history method 64 | 65 | 2.3.0 66 | ----- 67 | ## Added 68 | - `dividend_history` method that returns historical dividends paid for a given symbol(s) 69 | 70 | ## Fixed 71 | - `history` method has been refactored pretty heavily with the help of @maread99 (Thank you!). Timezone info is now included in the `date` column. Also, a dataframe will always be returned regardless of bad symbols existing. Previously, a dictionary was returned with the json response for the bad symbol(s) and dataframes for successful responses. 72 | 73 | 2.2.15 74 | ------ 75 | - Updated the data available from the cash flow statement 76 | 77 | 2.2.14 78 | ------ 79 | - Updated the financials dataframes (cash_flow, income_statement, balance_sheet, all_financial_data, 80 | get_financial_data) to include another column titled "currencyCode". This will identify the currency 81 | used in the financial statement. 82 | 83 | 2.2.13 84 | ------ 85 | - Fix bug related to dividends and stock splits. The merge statement to combine the pandas dataframes 86 | was using a left join instead of an outer join, which caused stock splits to drop. 87 | 88 | 2.2.11 89 | ------ 90 | - Fix bug with async requests and :code:`symbols` as a query parameter 91 | 92 | 2.2.9 93 | ----- 94 | - Fix internal method :code:`_format_data` in the :code:`_YahooFinance` class to account for dates held in lists 95 | - Use flit to publish package to pypi. Additionally, make selenium an optional package to install through :code:`pip install yahooquery[premium]` 96 | 97 | ## 2.2.8 98 | 99 | - `Ticker`, `Screener`, and `Research` classes now accept the keyword argument `progress`. If set to `True`, a progress bar will be displayed when downloading data. The default value is `False`. 100 | - Add a `search` function. This allows you to query Yahoo Finance for anything. Search for a company via cusip, ISIN number, name, etc. The function returns a dictionary containing most relevant quotes and news items. You can also provide an argument `first_quote` that returns only the most relevant quote from the query. 101 | - Add a `currency_converter` function. This will retrieve the current conversion rate between two specified currencies as well as historical rates over a specified period. 102 | - Fix bug related to converting financials (income statement, balance sheet, cash flow) to dataframe. 103 | 104 | ## 2.2.7 105 | 106 | - Fix bug related to ticker symbols with "&" 107 | - Add functionality to retrieve most recent 30 days of one minute interval data 108 | - Add Taiwan to the COUNTRIES dictionary 109 | 110 | ## 2.2.6 111 | 112 | - Allow premium subscribers to set environment variables for their Yahoo login credentials, 113 | specifically `YF_USERNAME` and `YF_PASSWORD` 114 | - Fix bug when validating symbols. If too many symbols are passed, the URL that's constructed 115 | becomes too long and a 414 error occurs 116 | - Fix bug related to login via Selenium 117 | - Enable `country` argument in miscellaneous functions 118 | - Add argument, `adj_ohlc`, to `history` method that allows user to adjust OHLC data based on adjusted close 119 | 120 | ## 2.2.5 121 | 122 | - Add more data accessors for the `Ticker` class: `quotes`, 123 | `corporate_events`, `all_financial_data`, `get_financial_data`, 124 | `corporate_guidance`, `p_all_financial_data`, and `p_get_financial_data` 125 | - Financials methods now include optional argument to include / exclude the trailing 126 | tweleve month (TTM) data. 127 | - The `history` method on the `Ticker` class now accepts an optional argument 128 | to adjust the timezone (`adj_timezone`) to the ticker's timezone. It defaults 129 | to `True`. 130 | - Further documentation of acceptable keyword arguments to the `Ticker` class. 131 | - `Ticker.news` is now a method. It accepts two arguments: `count` - 132 | number of items to return; `start` - start date to begin retrieving news items from 133 | - Bug fixes: `Ticker.history` method no longer returns extra rows when retrieving 134 | intraday data. 135 | 136 | ## 2.2.4 137 | 138 | - Increase the number of items available through the `income_statement`, 139 | `cash_flow`, `p_income_statement`, and `p_cash_flow` methods 140 | on the `Ticker` class. 141 | - Update how the session is initialized; specifically, include a timeout in the 142 | requests that are made, include a hook for bad status codes, and mount the 143 | adapter / retry to https requests 144 | 145 | ## 2.2.3 146 | 147 | - Add `valuation_measures` as a property to the `Ticker` class. 148 | Additionally, for Yahoo Finance premium subscribers, they can access the 149 | `p_valuation_measures` and supply either `a`, `q`, or 150 | `m` (annual, quarterly, monthly). The data returned with these can 151 | be seen in the `Statistics` tab through the Yahoo Finance front-end. 152 | 153 | 154 | ## 2.2.2 155 | 156 | - Fix bug in retrieving cash flow / income statement data. Most recent month was 157 | combining with TTM. A new column was created in the dataframe called 'periodType'. 158 | Annual data will be shown as '12M', quarterly data will be shown as '3M', and 159 | trailing 12 month data will be shown as 'TTM'. 160 | 161 | ## 2.2.1 162 | 163 | - Fix timestamp conversion in the `_format_data` method of the `_YahooFinance` class 164 | 165 | ## 2.2.0 166 | 167 | - New Research class that allows a user with a premium subscription to retrieve 168 | research reports and trade ideas from Yahoo Finance. List of trade ideas 169 | through Yahoo Finance can be seen at: https://finance.yahoo.com/research/trade-ideas. 170 | Research reports can be seen at https://finance.yahoo.com/research. 171 | 172 | ## 2.1.0 173 | 174 | - New Screener class that allows a user to retrieve predefined Yahoo 175 | Finance lists. Some of these lists include most active, day gainers, 176 | day losers, cryptocurrencies, and sectors / industries 177 | 178 | ## 2.0.0 179 | 180 | - Have Ticker class inherit from a base class, defined in base.py as 181 | `_YahooFinance`. The base class contains the order of operations to 182 | retrieve data (construct parameters, construct URLs, validate response, 183 | and format the data). 184 | - Yahoo login functionality, which allows a user to retrieve Premium data if they are a subscriber 185 | 186 | - All available financials data (income_statement, balance_sheet, cash_flow) 187 | - Company 360 (innovation score, significant developments, supply chain, 188 | hiring statistics, and company outlook) 189 | - Premium portal (research reports, trade ideas, technical events, value analyzer, 190 | and company snapshots) 191 | - Technical events 192 | - Value analyzer (High-level value analysis) 193 | - Value analyzer Drilldown (Detailed information about a symbol(s) value) 194 | - Research reports 195 | - Trade ideas 196 | 197 | - New (free) data! 198 | 199 | - `news` 200 | - `page_views` 201 | - `recommendations` 202 | - `technical_insights` 203 | - `validation` 204 | 205 | - Change several properties and methods (get_endpoints -> get_modules, 206 | all_endpoints -> all_modules) 207 | 208 | ## 1.1.3 209 | 210 | - Fix bug related to symbols that have characters that need to be url 211 | encoded (^) 212 | 213 | ## 1.1.2 214 | 215 | - Allow for user to use a string as a list of symbols to pass to Ticker class. 216 | For example, previous version would require user to pass 217 | `['fb', 'msft', 'goog']` to retrieve those three symbols. Now, the user 218 | can pass `'fb msft goog'` or `'fb,msft,goog'`. 219 | - Allow user to pass string, as well as list, to `get_endpoints` method. For 220 | example, `['assetProfile', 'balanceSheetHistory']` is equivalent to 221 | `'assetProfile balanceSheetHistory'`. 222 | 223 | ## 1.1.1 224 | 225 | - Fill NA values from history dataframe. Event data (dividends and splits) 226 | will be filled with zeros. Other columns (high, low, open, close, 227 | volume, adjclose) will be filled with prior day's data. 228 | - Fill NA values from options dataframe. Missing values are replaced with zero 229 | 230 | ## 1.1.0 231 | 232 | - Entire library makes asynchronous requests (missing piece was the 233 | option_chain method). 234 | 235 | ## 1.0.15 236 | 237 | - Missing required library requests-futures in setup.py file 238 | 239 | ## 1.0.14 240 | 241 | - Add asynchronous requests with the requests-futures library 242 | - Add "events" to the history dataframe (dividends and splits) 243 | 244 | ## 1.0.13 245 | 246 | - Add `adjclose` column to dataframe returned from `yahooquery.Ticker.history` 247 | 248 | ## 1.0.12 249 | 250 | - Changed private Ticker variables (_ENDPOINTS, _PERIODS, and _INTERVALS) 251 | to public 252 | - Updated README for new multiple endpoint methods as well as a comparison 253 | to yfinance 254 | - Forced dictionary return when formatted = False. 255 | 256 | ## 1.0.11 257 | 258 | - Bug fix related to accessing the multiple endpoint methods 259 | (get_endpoints, all_endpoints). Error would occur during 260 | formatting, specifically for the earningsTrend endpoint 261 | - Bug fix related to passing one endpoint to the get_endpoints 262 | method. 263 | 264 | ## 1.0.10 265 | - 266 | - Added docstrings to each property / method 267 | - Changed get_multiple_endpoints method to get_endpoints 268 | - Added all known endpoints into Ticker class. Missing 269 | endpoints were earnings, earnings_trend, and index_trend 270 | 271 | ## 1.0.9 272 | 273 | - Removed combine_dataframes kwarg. This is just the default behavior now. 274 | - Removed ticker column in history method. `symbol` is now part of 275 | a MultiIndex in the returned DataFrame 276 | 277 | ## 1.0.8 278 | 279 | - Updated option_chain method for bugs as well as MultiIndex indexing 280 | to allow the user an easier way to make cross-sections of the 281 | resulting data. 282 | 283 | ## 1.0.7 284 | 285 | - Made the symbols argument to the `Ticker` class a required argument 286 | - Fixed bug related to the `fund_category_holdings` property. 287 | - Fixed bug related to the `history` method. 288 | - Added tests and initial attempt at Travis CI 289 | 290 | ## 1.0.6 291 | 292 | - Added frequency arguments to `balance_sheet`, `cash_flow`, and 293 | `income_statement` methods. They will default to annual, but can 294 | return quarterly statements with "q" or "Q" arguments. 295 | - Added a `calendar_events` property to the `Ticker` class. 296 | Shows next earnings date, previous dividend date, and other metrics. 297 | 298 | ## 1.0.5 299 | 300 | - Fixed bug related to formatting empty lists 301 | 302 | ## 1.0.4 303 | 304 | - Add `fund_performance` property to the `Ticker` class. Shows 305 | historical fund performance as well as category performance. 306 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Change Log 2 | ========== 3 | 4 | 2.4.1 5 | ----- 6 | ## Fix 7 | - Fix bug related to `period2` in `fundamentals` endpoint 8 | 9 | 2.4.0 10 | ----- 11 | ## Update 12 | - Update to use uv for packaging and publishing 13 | - Folder, file structure 14 | - `validate_symbols` method now returns a tuple of valid and invalid symbols 15 | 16 | ## Add 17 | - `curl_cffi` to dependencies instead of `requests` for session management 18 | 19 | 2.3.7 20 | ----- 21 | ## Add 22 | - Logic for handling setting up a session when a consent screen is encountered. This is primarily seen in European countries 23 | and should allow for the continued use of this package. 24 | - Keyword argument, `setup_url`, to the base `_YahooFinance` class that allows a user to override the url used in setting up the session. As a default 25 | the Yahoo Finance home page is used (https://finance.yahoo.com). You can also create an environment variable, `YF_SETUP_URL` that will be used if set. 26 | Example usage: 27 | ```python 28 | import yahooquery as yq 29 | 30 | t = yq.Ticker('aapl', setup_url='https://finance.yahoo.com/quote/AAPL') 31 | ``` 32 | 33 | ## Remove 34 | - Webdriver manager is no longer used internally. Selenium Manager is now fully included with selenium `4.10.0`, so this package is no longer needed. 35 | 36 | 2.3.6 37 | ----- 38 | ## Fix 39 | - Use the previously instantiated session within the `Ticker` class (that may also contain proxies, other important session info) to retrieve both cookies and a crumb 40 | 41 | 2.3.5 42 | ----- 43 | ## Fix 44 | - Fix bad assignment to crumb when using username/password for premium access 45 | 46 | 2.3.4 47 | ----- 48 | ## Update 49 | - Use a different url to try and obtain cookies (fc.yahoo.com no longer works) 50 | - Refactor how a session is initialized 51 | - Use the country as a way to make the request to obtain cookies be more location specific 52 | 53 | 2.3.3 54 | ----- 55 | ## Update 56 | - Try and obtain cookies and a crumb during `Ticker` initialization so they can be used in further requests. 57 | 58 | 2.3.2 59 | ----- 60 | ## Update 61 | - Update quote summary endpoint from v10 to v6. The v10 endpoint currently requires a crumb as a query parameter, which is not something this library does not currently support. 62 | 63 | ## Fix 64 | - Bug related to retrieving screen IDs with a number 65 | 66 | 2.3.1 67 | ----- 68 | ## Fix 69 | - Fixes for history method 70 | 71 | 2.3.0 72 | ----- 73 | ## Added 74 | - `dividend_history` method that returns historical dividends paid for a given symbol(s) 75 | 76 | ## Fixed 77 | - `history` method has been refactored pretty heavily with the help of @maread99 (Thank you!). Timezone info is now included in the `date` column. Also, a dataframe will always be returned regardless of bad symbols existing. Previously, a dictionary was returned with the json response for the bad symbol(s) and dataframes for successful responses. 78 | 79 | 2.2.15 80 | ------ 81 | - Updated the data available from the cash flow statement 82 | 83 | 2.2.14 84 | ------ 85 | - Updated the financials dataframes (cash_flow, income_statement, balance_sheet, all_financial_data, 86 | get_financial_data) to include another column titled "currencyCode". This will identify the currency 87 | used in the financial statement. 88 | 89 | 2.2.13 90 | ------ 91 | - Fix bug related to dividends and stock splits. The merge statement to combine the pandas dataframes 92 | was using a left join instead of an outer join, which caused stock splits to drop. 93 | 94 | 2.2.11 95 | ------ 96 | - Fix bug with async requests and :code:`symbols` as a query parameter 97 | 98 | 2.2.9 99 | ----- 100 | - Fix internal method :code:`_format_data` in the :code:`_YahooFinance` class to account for dates held in lists 101 | - Use flit to publish package to pypi. Additionally, make selenium an optional package to install through :code:`pip install yahooquery[premium]` 102 | 103 | 2.2.8 104 | ----- 105 | - :code:`Ticker`, :code:`Screener`, and :code:`Research` classes now accept the 106 | keyword argument :code:`progress`. If set to :code:`True`, a progress bar will 107 | be displayed when downloading data. The default value is :code:`False` 108 | - Add a :code:`search` function. This allows you to query Yahoo Finance for anything. 109 | Search for a company via cusip, ISIN Number, name, etc. The function returns a dictionary 110 | containing most relevant quotes and news items. You can also provide an argument :code:`first_quote` 111 | that returns only the most relevant quote from the query 112 | - Add a :code:`currency_converter` function. This will retrieve the current conversion rate between 113 | two specified currencies as well as historical rates over a specified period. 114 | - Fix bug related to converting financials (income statement, balance sheet, cash flow) to dataframe. 115 | - The :code:`symbols` argument to the :code:`Ticker` class now accepts any iterable. The strings 116 | will also be converted to uppercase. 117 | 118 | 2.2.7 119 | ----- 120 | - Fix bug related to ticker symbols with "&" 121 | - Add functionality to retrieve most recent 30 days of one minute interval data 122 | - Add Taiwan to the COUNTRIES dictionary 123 | 124 | 2.2.6 125 | ----- 126 | - Allow premium subscribers to set environment variables for their Yahoo login credentials, 127 | specifically YF_USERNAME and YF_PASSWORD 128 | - Fix bug when validating symbols. If too many symbols are passed, the URL that's constructed 129 | becomes too long and a 414 error occurs 130 | - Fix bug related to login via Selenium 131 | - Enable country argument in miscellaneous functions 132 | - Add argument to history method that allows user to adjust OHLC data based on adjusted close 133 | 134 | 2.2.5 135 | ----- 136 | - Add more data accessors for the :code:`Ticker` class: :code:`quotes`, 137 | :code:`corporate_events`, :code:`all_financial_data`, :code:`get_financial_data`, 138 | :code:`corporate_guidance`, :code:`p_all_financial_data`, and :code:`p_get_financial_data` 139 | - Financials methods now include optional argument to include / exclude the trailing 140 | tweleve month (TTM) data. 141 | - The :code:`history` method on the :code:`Ticker` class now accepts an optional argument 142 | to adjust the timezone (:code:`adj_timezone`) to the ticker's timezone. It defaults 143 | to :code:`True`. 144 | - Further documentation of acceptable keyword arguments to the :code:`Ticker` class. 145 | - :code:`Ticker.news` is now a method. It accepts two arguments: :code:`count` - 146 | number of items to return; :code:`start` - start date to begin retrieving news items from 147 | - Bug fixes: :code:`Ticker.history` method no longer returns extra rows when retrieving 148 | intraday data. 149 | 150 | 2.2.4 151 | ----- 152 | - Increase the number of items available through the :code:`income_statement`, 153 | :code:`cash_flow`, :code:`p_income_statement`, and :code:`p_cash_flow` methods 154 | on the :code:`Ticker` class. 155 | - Update how the session is initialized; specifically, include a timeout in the 156 | requests that are made, include a hook for bad status codes, and mount the 157 | adapter / retry to https requests 158 | 159 | 2.2.3 160 | ----- 161 | - Add :code:`valuation_measures` as a property to the :code:`Ticker` class. 162 | Additionally, for Yahoo Finance premium subscribers, they can access the 163 | :code:`p_valuation_measures` and supply either :code:`a`, :code:`q`, or 164 | :code:`m` (annual, quarterly, monthly). The data returned with these can 165 | be seen in the `Statistics` tab through the Yahoo Finance front-end. 166 | 167 | .. image:: demo/valuation_measures.PNG 168 | 169 | 2.2.2 170 | ----- 171 | - Fix bug in retrieving cash flow / income statement data. Most recent month was 172 | combining with TTM. A new column was created in the dataframe called 'periodType'. 173 | Annual data will be shown as '12M', quarterly data will be shown as '3M', and 174 | trailing 12 month data will be shown as 'TTM'. 175 | 176 | 2.2.1 177 | ----- 178 | - Fix timestamp conversion in the _format_data method of the _YahooFinance class 179 | 180 | 2.2.0 181 | ----- 182 | - New Research class that allows a user with a premium subscription to retrieve 183 | research reports and trade ideas from Yahoo Finance. List of trade ideas 184 | through Yahoo Finance can be seen at: https://finance.yahoo.com/research/trade-ideas. 185 | Research reports can be seen at https://finance.yahoo.com/research. 186 | 187 | 2.1.0 188 | ----- 189 | - New Screener class that allows a user to retrieve predefined Yahoo 190 | Finance lists. Some of these lists include most active, day gainers, 191 | day losers, cryptocurrencies, and sectors / industries 192 | 193 | 2.0.0 194 | ----- 195 | - Have Ticker class inherit from a base class, defined in base.py as 196 | _YahooFinance. The base class contains the order of operations to 197 | retrieve data (construct parameters, construct URLs, validate response, 198 | and format the data). 199 | - Yahoo login functionality, which allows a user to retrieve Premium data if they are a subscriber 200 | 201 | - All available financials data (income_statement, balance_sheet, cash_flow) 202 | - Company 360 (innovation score, significant developments, supply chain, 203 | hiring statistics, and company outlook) 204 | - Premium portal (research reports, trade ideas, technical events, value analyzer, 205 | and company snapshots) 206 | - Technical events 207 | - Value analyzer (High-level value analysis) 208 | - Value analyzer Drilldown (Detailed information about a symbol(s) value) 209 | - Research reports 210 | - Trade ideas 211 | 212 | - New (free) data! 213 | 214 | - news 215 | - page_views 216 | - recommendations 217 | - technical_insights 218 | - validation 219 | 220 | - Change several properties and methods (get_endpoints -> get_modules, 221 | all_endpoints -> all_modules) 222 | 223 | 1.1.3 224 | ----- 225 | - Fix bug related to symbols that have characters that need to be url 226 | encoded (^) 227 | 228 | 1.1.2 229 | ----- 230 | - Allow for user to use a string as a list of symbols to pass to Ticker class. 231 | For example, previous version would require user to pass 232 | `['fb', 'msft', 'goog']` to retrieve those three symbols. Now, the user 233 | can pass `'fb msft goog'` or `'fb,msft,goog'`. 234 | - Allow user to pass string, as well as list, to `get_endpoints` method. For 235 | example, `['assetProfile', 'balanceSheetHistory']` is equivalent to 236 | `'assetProfile balanceSheetHistory'`. 237 | 238 | 1.1.1 239 | ----- 240 | - Fill NA values from history dataframe. Event data (dividends and splits) 241 | will be filled with zeros. Other columns (high, low, open, close, 242 | volume, adjclose) will be filled with prior day's data. 243 | - Fill NA values from options dataframe. Missing values are replaced with zero 244 | 245 | 1.1.0 246 | ----- 247 | - Entire library makes asynchronous requests (missing piece was the 248 | option_chain method). 249 | 250 | 1.0.15 251 | ------ 252 | - Missing required library requests-futures in setup.py file 253 | 254 | 1.0.14 255 | ------ 256 | - Add asynchronous requests with the requests-futures library 257 | - Add "events" to the history dataframe (dividends and splits) 258 | 259 | 1.0.13 260 | ------ 261 | - Add `adjclose` column to dataframe returned from `yahooquery.Ticker.history` 262 | 263 | 1.0.12 264 | ------ 265 | - Changed private Ticker variables (_ENDPOINTS, _PERIODS, and _INTERVALS) 266 | to public 267 | - Updated README for new multiple endpoint methods as well as a comparison 268 | to yfinance 269 | - Forced dictionary return when formatted = False. 270 | 271 | 1.0.11 272 | ------ 273 | - Bug fix related to accessing the multiple endpoint methods 274 | (get_endpoints, all_endpoints). Error would occur during 275 | formatting, specifically for the earningsTrend endpoint 276 | - Bug fix related to passing one endpoint to the get_endpoints 277 | method. 278 | 279 | 1.0.10 280 | ------ 281 | - Added docstrings to each property / method 282 | - Changed get_multiple_endpoints method to get_endpoints 283 | - Added all known endpoints into Ticker class. Missing 284 | endpoints were earnings, earnings_trend, and index_trend 285 | 286 | 1.0.9 287 | ----- 288 | - Removed combine_dataframes kwarg. This is just the default behavior now. 289 | - Removed ticker column in history method. `symbol` is now part of 290 | a MultiIndex in the returned DataFrame 291 | 292 | 1.0.8 293 | ----- 294 | - Updated option_chain method for bugs as well as MultiIndex indexing 295 | to allow the user an easier way to make cross-sections of the 296 | resulting data. 297 | 298 | 1.0.7 299 | ----- 300 | - Made the symbols argument to the `Ticker` class a required argument 301 | - Fixed bug related to the `fund_category_holdings` property. 302 | - Fixed bug related to the `history` method. 303 | - Added tests and initial attempt at Travis CI 304 | 305 | 1.0.6 306 | ----- 307 | - Added frequency arguments to `balance_sheet`, `cash_flow`, and 308 | `income_statement` methods. They will default to annual, but can 309 | return quarterly statements with "q" or "Q" arguments. 310 | - Added a `calendar_events` property to the `Ticker` class. 311 | Shows next earnings date, previous dividend date, and other metrics. 312 | 313 | 1.0.5 314 | ----- 315 | - Fixed bug related to formatting empty lists 316 | 317 | 1.0.4 318 | ------- 319 | - Add `fund_performance` property to the `Ticker` class. Shows 320 | historical fund performance as well as category performance. 321 | -------------------------------------------------------------------------------- /yahooquery/base.py: -------------------------------------------------------------------------------- 1 | # stdlib 2 | import logging 3 | import os 4 | from concurrent.futures import as_completed 5 | from datetime import datetime 6 | from typing import ClassVar 7 | from urllib import parse 8 | 9 | # third party 10 | from requests_futures.sessions import FuturesSession 11 | from tqdm import tqdm 12 | 13 | # first party 14 | from yahooquery.constants import ( 15 | CONFIG, 16 | COUNTRIES, 17 | ) 18 | from yahooquery.headless import YahooFinanceHeadless, has_selenium 19 | from yahooquery.session_management import get_crumb, initialize_session 20 | from yahooquery.utils import convert_to_list 21 | 22 | logger = logging.getLogger(__name__) 23 | 24 | 25 | class _YahooFinance: 26 | CHUNK: ClassVar[int] = 1500 27 | 28 | def __init__(self, **kwargs): 29 | self.country = kwargs.pop("country", "united states").lower() 30 | self.formatted = kwargs.pop("formatted", False) 31 | self.progress = kwargs.pop("progress", False) 32 | self.username = kwargs.pop("username", os.getenv("YF_USERNAME", None)) 33 | self.password = kwargs.pop("password", os.getenv("YF_PASSWORD", None)) 34 | self._setup_url = kwargs.pop("setup_url", os.getenv("YF_SETUP_URL", None)) 35 | self.session = initialize_session(kwargs.pop("session", None), **kwargs) 36 | if self.username and self.password: 37 | self.login() 38 | self.crumb = get_crumb(self.session) 39 | 40 | @property 41 | def symbols(self): 42 | """ 43 | List of symbol(s) used to retrieve information 44 | """ 45 | return self._symbols 46 | 47 | @symbols.setter 48 | def symbols(self, symbols): 49 | self._symbols = convert_to_list(symbols) 50 | 51 | @property 52 | def country(self): 53 | return self._country 54 | 55 | @country.setter 56 | def country(self, country): 57 | if country.lower() not in COUNTRIES: 58 | raise ValueError( 59 | "{} is not a valid country. Valid countries include {}".format( 60 | country, ", ".join(COUNTRIES.keys()) 61 | ) 62 | ) 63 | self._country = country.lower() 64 | self._country_params = COUNTRIES[self._country] 65 | 66 | @property 67 | def default_query_params(self): 68 | """ 69 | Dictionary containing default query parameters that are sent with 70 | each request. The dictionary contains four keys: lang, region, 71 | corsDomain, and crumb 72 | 73 | Notes 74 | ----- 75 | The query parameters will default to 76 | {'lang': 'en-US', 'region': 'US', 'corsDomain': 'finance.yahoo.com'} 77 | 78 | To change the default query parameters, set the country property equal 79 | to a valid country. 80 | """ 81 | params = self._country_params 82 | if self.crumb is not None: 83 | params["crumb"] = self.crumb 84 | return params 85 | 86 | def login(self) -> None: 87 | if has_selenium: 88 | instance = YahooFinanceHeadless(self.username, self.password) 89 | instance.login() 90 | if instance.cookies: 91 | self.session.cookies = instance.cookies 92 | return 93 | 94 | else: 95 | logger.warning( 96 | "Unable to login and/or retrieve the appropriate cookies. This is " 97 | "most likely due to Yahoo Finance instituting recaptcha, which " 98 | "this package does not support." 99 | ) 100 | 101 | else: 102 | logger.warning( 103 | "You do not have the required libraries to use this feature. Install " 104 | "with the following: `pip install yahooquery[premium]`" 105 | ) 106 | 107 | def _chunk_symbols(self, key, params=None, chunk=None, **kwargs): 108 | current_symbols = self.symbols 109 | all_data = [] if kwargs.get("list_result") else {} 110 | chunk = chunk or self.CHUNK 111 | for i in tqdm(range(0, len(current_symbols), chunk), disable=not self.progress): 112 | self._symbols = current_symbols[i : i + chunk] 113 | data = self._get_data(key, params, disable=True, **kwargs) 114 | if isinstance(data, str): 115 | self._symbols = current_symbols 116 | return data 117 | all_data.extend(data) if isinstance(all_data, list) else all_data.update( 118 | data 119 | ) 120 | self.symbols = current_symbols 121 | return all_data 122 | 123 | def validate_symbols(self) -> tuple[list[str], list[str]]: 124 | """Symbol Validation 125 | 126 | Validate existence of given symbol(s) and modify the symbols property 127 | to include only the valid symbols. If invalid symbols were passed, 128 | they will be stored in the `invalid_symbols` property. 129 | """ 130 | data = self._chunk_symbols("validation") 131 | valid_symbols = [] 132 | invalid_symbols = [] 133 | for k, v in data.items(): 134 | if v: 135 | valid_symbols.append(k) 136 | else: 137 | invalid_symbols.append(k) 138 | 139 | return valid_symbols, invalid_symbols 140 | 141 | def _format_data(self, obj, dates): 142 | for k, v in obj.items(): 143 | if k in dates: 144 | if isinstance(v, dict): 145 | obj[k] = v.get("fmt", v) 146 | elif isinstance(v, list): 147 | try: 148 | obj[k] = [item.get("fmt") for item in v] 149 | except AttributeError: 150 | obj[k] = [ 151 | datetime.fromtimestamp(date).strftime("%Y-%m-%d %H:%M:S") 152 | for date in v 153 | ] 154 | else: 155 | try: 156 | obj[k] = datetime.fromtimestamp(v).strftime("%Y-%m-%d %H:%M:%S") 157 | except (TypeError, OSError): 158 | obj[k] = v 159 | elif isinstance(v, dict): 160 | if "raw" in v: 161 | obj[k] = v.get("raw") 162 | elif "min" in v: 163 | obj[k] = v 164 | else: 165 | obj[k] = self._format_data(v, dates) 166 | elif isinstance(v, list): 167 | if len(v) == 0: 168 | obj[k] = v 169 | elif isinstance(v[0], dict): 170 | for i, list_item in enumerate(v): 171 | obj[k][i] = self._format_data(list_item, dates) 172 | else: 173 | obj[k] = v 174 | else: 175 | obj[k] = v 176 | return obj 177 | 178 | def _get_data(self, key, params=None, **kwargs): 179 | config = CONFIG[key] 180 | params = self._construct_params(config, params) 181 | urls = self._construct_urls(config, params, **kwargs) 182 | response_field = config["response_field"] 183 | try: 184 | if isinstance(self.session, FuturesSession): 185 | data = self._async_requests(response_field, urls, params, **kwargs) 186 | else: 187 | data = self._sync_requests(response_field, urls, params, **kwargs) 188 | return data 189 | except ValueError: 190 | return {"error": "HTTP 404 Not Found. Please try again"} 191 | 192 | def _construct_params(self, config, params=None): 193 | params = params or {} 194 | required_params = [ 195 | k 196 | for k in config["query"] 197 | if config["query"][k]["required"] and "symbol" not in k 198 | ] 199 | for required in required_params: 200 | if not params.get(required): 201 | params.update( 202 | { 203 | required: getattr( 204 | self, required, config["query"][required]["default"] 205 | ) 206 | } 207 | ) 208 | optional_params = [ 209 | k 210 | for k in config["query"] 211 | if not config["query"][k]["required"] 212 | and config["query"][k]["default"] is not None 213 | ] 214 | for optional in optional_params: 215 | params.update( 216 | { 217 | optional: getattr( 218 | self, optional, config["query"][optional]["default"] 219 | ) 220 | } 221 | ) 222 | params.update(self.default_query_params) 223 | params = { 224 | k: str(v).lower() if v is True or v is False else v 225 | for k, v in params.items() 226 | } 227 | if "symbol" in config["query"]: 228 | return [dict(params, symbol=symbol) for symbol in self._symbols] 229 | return params 230 | 231 | def _construct_urls(self, config, params, **kwargs): 232 | """Construct URL requests""" 233 | if kwargs.get("method") == "post": 234 | urls = [ 235 | self.session.post( 236 | url=config["path"], params=params, json=kwargs.get("payload") 237 | ) 238 | ] 239 | elif "symbol" in config["query"]: 240 | ls = ( 241 | params 242 | if isinstance(self.session, FuturesSession) 243 | else tqdm(params, disable=not self.progress) 244 | ) 245 | urls = [self.session.get(url=config["path"], params=p) for p in ls] 246 | elif "symbols" in config["query"]: 247 | params.update({"symbols": ",".join(self._symbols)}) 248 | urls = [self.session.get(url=config["path"], params=params)] 249 | else: 250 | ls = ( 251 | self._symbols 252 | if isinstance(self.session, FuturesSession) 253 | else tqdm(self._symbols, disable=not self.progress) 254 | ) 255 | urls = [ 256 | self.session.get( 257 | url=config["path"].format(**{"symbol": symbol}), params=params 258 | ) 259 | for symbol in ls 260 | ] 261 | return urls 262 | 263 | def _async_requests(self, response_field, urls, params, **kwargs): 264 | data = {} 265 | for future in tqdm( 266 | as_completed(urls), 267 | total=len(urls), 268 | disable=kwargs.get("disable", not self.progress), 269 | ): 270 | response = future.result() 271 | json = self._validate_response(response.json(), response_field) 272 | symbol = self._get_symbol(response, params) 273 | if symbol is not None: 274 | data[symbol] = self._construct_data(json, response_field, **kwargs) 275 | else: 276 | data = self._construct_data(json, response_field, **kwargs) 277 | return data 278 | 279 | def _sync_requests(self, response_field, urls, params, **kwargs): 280 | data = {} 281 | for response in urls: 282 | json = self._validate_response(response.json(), response_field) 283 | symbol = self._get_symbol(response, params) 284 | if symbol is not None: 285 | data[symbol] = self._construct_data(json, response_field, **kwargs) 286 | else: 287 | data = self._construct_data(json, response_field, **kwargs) 288 | return data 289 | 290 | def _validate_response(self, response, response_field): 291 | try: 292 | if response[response_field]["error"]: 293 | error = response[response_field]["error"] 294 | return error.get("description") 295 | if not response[response_field]["result"]: 296 | return "No data found" 297 | return response 298 | except KeyError: 299 | if "finance" in response: 300 | if response["finance"].get("error"): 301 | return response["finance"]["error"]["description"] 302 | return response 303 | return {response_field: {"result": [response]}} 304 | 305 | def _get_symbol(self, response, params): 306 | if isinstance(params, list): 307 | query_params = dict(parse.parse_qsl(parse.urlsplit(response.url).query)) 308 | return query_params["symbol"] 309 | if "symbols" in params: 310 | return None 311 | return parse.unquote(response.url.rsplit("/")[-1].split("?")[0]) 312 | 313 | def _construct_data(self, json, response_field, **kwargs): 314 | try: 315 | addl_key = kwargs.get("addl_key") 316 | if addl_key: 317 | data = json[response_field]["result"][0][addl_key] 318 | elif kwargs.get("list_result", False): 319 | data = json[response_field]["result"] 320 | else: 321 | data = json[response_field]["result"][0] 322 | except KeyError: 323 | data = ( 324 | json[response_field]["result"][addl_key] 325 | if addl_key 326 | else json[response_field]["result"] 327 | ) 328 | except TypeError: 329 | data = json 330 | return data 331 | -------------------------------------------------------------------------------- /tests/test_ticker.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import itertools 3 | import os 4 | 5 | import pandas as pd 6 | import pytest 7 | from pandas.testing import assert_frame_equal, assert_index_equal, assert_series_equal 8 | 9 | from yahooquery import Ticker 10 | from yahooquery.utils import history_dataframe 11 | 12 | TICKERS = [ 13 | Ticker( 14 | "aapl", username=os.getenv("YFF_USERNAME"), password=os.getenv("YFF_PASSWORD") 15 | ), 16 | Ticker("aapl ^GSPC btcusd=x brk-b logo.is l&tfh.ns", asynchronous=True), 17 | Ticker("aaapl"), 18 | Ticker("hasgx"), 19 | Ticker("btcusd=x", formatted=True, validate=True), 20 | ] 21 | 22 | FINANCIALS = [ 23 | "cash_flow", 24 | "income_statement", 25 | "balance_sheet", 26 | "p_cash_flow", 27 | "p_income_statement", 28 | "p_balance_sheet", 29 | "all_financial_data", 30 | "p_all_financial_data", 31 | "p_valuation_measures", 32 | ] 33 | 34 | SEPERATE_ENDPOINTS = [ 35 | *FINANCIALS, 36 | "option_chain", 37 | "history", 38 | "all_modules", 39 | "get_modules", 40 | "symbols", 41 | "p_portal", 42 | "p_value_analyzer", 43 | ] 44 | 45 | 46 | def props(cls): 47 | return [ 48 | i 49 | for i in cls.__dict__.keys() 50 | if i[:1] != "_" and i[:2] != "p_" and i not in SEPERATE_ENDPOINTS 51 | ] 52 | 53 | 54 | def premium_props(cls): 55 | return [i for i in cls.__dict__.keys() if i[:2] == "p_"] 56 | 57 | 58 | @pytest.mark.parametrize("prop", premium_props(Ticker)) 59 | def test_premium(ticker, prop): 60 | assert getattr(ticker, prop) is not None 61 | 62 | 63 | @pytest.fixture(params=TICKERS) 64 | def ticker(request): 65 | return request.param 66 | 67 | 68 | def test_symbols_change(ticker): 69 | ticker.symbols = "aapl msft fb" 70 | assert ticker.symbols == ["aapl", "msft", "fb"] 71 | 72 | 73 | def test_p_reports(ticker): 74 | assert ticker.p_reports("26426_Technical Analysis_1584057600000") 75 | 76 | 77 | def test_p_ideas(ticker): 78 | assert ticker.p_ideas("tc_USvyGmAAlpfwAygABAACAAAD6CYg") 79 | 80 | 81 | def test_option_chain(ticker): 82 | assert ticker.option_chain is not None 83 | 84 | 85 | def test_bad_multiple_modules_wrong(ticker): 86 | with pytest.raises(ValueError): 87 | assert ticker.get_modules(["asetProfile", "summaryProfile"]) 88 | 89 | 90 | def test_multiple_modules(ticker): 91 | assert ticker.get_modules(["assetProfile", "summaryProfile"]) is not None 92 | 93 | 94 | def test_multiple_modules_str(ticker): 95 | assert ticker.get_modules("assetProfile summaryProfile") is not None 96 | 97 | 98 | def test_news(ticker): 99 | assert ticker.news() is not None 100 | 101 | 102 | def test_news_start(ticker): 103 | assert ticker.news(start="2020-01-01", count=100) is not None 104 | 105 | 106 | def test_all_modules(ticker): 107 | assert ticker.all_modules is not None 108 | data = ticker.all_modules 109 | assert sorted(list(data.keys())) == sorted(ticker.all_modules) 110 | 111 | 112 | @pytest.mark.parametrize("module", props(Ticker)) 113 | def test_modules(ticker, module): 114 | assert getattr(ticker, module) is not None 115 | 116 | 117 | @pytest.mark.parametrize( 118 | "module, frequency", [el for el in itertools.product(FINANCIALS, ["q", "a"])] 119 | ) 120 | def test_financials(ticker, frequency, module): 121 | assert getattr(ticker, module)(frequency) is not None 122 | 123 | 124 | def test_bad_financials_arg(): 125 | with pytest.raises(KeyError): 126 | assert Ticker("aapl").income_statement("r") 127 | 128 | 129 | def test_get_financial_data(ticker): 130 | result = ticker.get_financial_data( 131 | "GrossProfit NetIncome TotalAssets ForwardPeRatio" 132 | ) 133 | assert result is not None 134 | if isinstance(result, str): 135 | assert "unavailable" in result.lower() 136 | else: 137 | assert isinstance(result, pd.DataFrame) 138 | 139 | 140 | def test_p_get_financial_data(ticker): 141 | assert ( 142 | ticker.p_get_financial_data("GrossProfit NetIncome TotalAssets ForwardPeRatio") 143 | is not None 144 | ) 145 | 146 | 147 | @pytest.mark.parametrize( 148 | "period, interval", 149 | [ 150 | (p, i) 151 | for p, i in zip( 152 | ["1d", "1mo", "1y", "5y", "max"], ["1m", "1m", "1d", "1wk", "3mo"] 153 | ) 154 | ], 155 | ) 156 | def test_history(ticker, period, interval): 157 | assert isinstance(ticker.history(period, interval), pd.DataFrame) 158 | 159 | 160 | def test_dividend_history(ticker): 161 | df = ticker.dividend_history(start="1970-01-01") 162 | assert isinstance(df, pd.DataFrame) 163 | 164 | 165 | @pytest.mark.parametrize( 166 | "start, end", 167 | [ 168 | (start, end) 169 | for start, end in zip( 170 | [datetime.datetime(2019, 1, 1), "2019-01-01"], 171 | ["2019-12-30", datetime.datetime(2019, 12, 30)], 172 | ) 173 | ], 174 | ) 175 | def test_history_start_end(ticker, start, end): 176 | assert ticker.history(start=start, end=end) is not None 177 | 178 | 179 | @pytest.mark.parametrize( 180 | "period, interval", [(p, i) for p, i in zip(["2d", "1mo"], ["1m", "3m"])] 181 | ) 182 | def test_history_bad_args(ticker, period, interval): 183 | with pytest.raises(ValueError): 184 | assert ticker.history(period, interval) 185 | 186 | 187 | def test_adj_ohlc(ticker): 188 | assert ticker.history(period="max", adj_ohlc=True) is not None 189 | 190 | 191 | class TestHistoryDataframe: 192 | """Tests for `utils.history_dataframe` and dependencies.""" 193 | 194 | @pytest.fixture 195 | def tz_us(self): 196 | yield "America/New_York" 197 | 198 | @pytest.fixture 199 | def tz_oz(self): 200 | yield "Australia/Sydney" 201 | 202 | @pytest.fixture 203 | def tz_hk(self): 204 | yield "Asia/Hong_Kong" 205 | 206 | @pytest.fixture 207 | def utc(self): 208 | yield "UTC" 209 | 210 | @pytest.fixture 211 | def timestamps_daily(self, utc, tz_oz, tz_us, tz_hk): 212 | """Timestamps representing fictional open datetimes and expected mapped days. 213 | 214 | Expected conversions to specific timezones explicitly declared and asserted. 215 | 216 | Yields 217 | ------- 218 | tuple[list[int] 219 | [0] [list[int]] 220 | Unix timestamps, i.e. format as used by Yahoo API. Timestamps represent 221 | datetimes of session opens in terms of UTC. 222 | 223 | [1] pd.Index dtype 'object', values as type datetime.date 224 | Expected days that timestamps would map to if local timezone were 225 | 'America/New_York'. In this case all timestamps are expected to map to 226 | the day of the date of the timestamp. 227 | 228 | [2] pd.Index dtype 'object', values as type datetime.date 229 | Expected days that timestamps would map to if local timezone were 230 | 'Australia/Sydney'. In this case all timestamps are expected to map to 231 | the day after the date of the timestamp. 232 | 233 | [3] pd.Index dtype 'object', values as type datetime.date 234 | Expected days that timestamps would map to if local timezone were 'UTC'. 235 | The first timestamp is expected to map to day of the date of the 236 | timestamp. All other timestamps are expected to map to the day after. 237 | """ 238 | tss = [ 239 | 1667568600, 240 | 1667831400, 241 | 1667917800, 242 | 1668004200, 243 | 1668090600, 244 | 1668177000, 245 | 1668436200, 246 | 1668522600, 247 | 1668609000, 248 | 1668695400, 249 | 1668781800, 250 | 1669041000, 251 | 1669127400, 252 | 1669213800, 253 | ] 254 | 255 | expected_utc = pd.DatetimeIndex( 256 | [ 257 | "2022-11-04 13:30:00", 258 | "2022-11-07 14:30:00", 259 | "2022-11-08 14:30:00", 260 | "2022-11-09 14:30:00", 261 | "2022-11-10 14:30:00", 262 | "2022-11-11 14:30:00", 263 | "2022-11-14 14:30:00", 264 | "2022-11-15 14:30:00", 265 | "2022-11-16 14:30:00", 266 | "2022-11-17 14:30:00", 267 | "2022-11-18 14:30:00", 268 | "2022-11-21 14:30:00", 269 | "2022-11-22 14:30:00", 270 | "2022-11-23 14:30:00", 271 | ], 272 | tz=utc, 273 | ) 274 | dti = pd.to_datetime(tss, unit="s") 275 | dti_utc = dti.tz_localize(utc) 276 | assert_index_equal(dti_utc, expected_utc) 277 | expected_utc_days = pd.Index( 278 | [ 279 | datetime.date(2022, 11, 4), 280 | datetime.date(2022, 11, 8), 281 | datetime.date(2022, 11, 9), 282 | datetime.date(2022, 11, 10), 283 | datetime.date(2022, 11, 11), 284 | datetime.date(2022, 11, 12), 285 | datetime.date(2022, 11, 15), 286 | datetime.date(2022, 11, 16), 287 | datetime.date(2022, 11, 17), 288 | datetime.date(2022, 11, 18), 289 | datetime.date(2022, 11, 19), 290 | datetime.date(2022, 11, 22), 291 | datetime.date(2022, 11, 23), 292 | datetime.date(2022, 11, 24), 293 | ] 294 | ) 295 | 296 | expected_oz = pd.DatetimeIndex( 297 | [ 298 | "2022-11-05 00:30:00", 299 | "2022-11-08 01:30:00", 300 | "2022-11-09 01:30:00", 301 | "2022-11-10 01:30:00", 302 | "2022-11-11 01:30:00", 303 | "2022-11-12 01:30:00", 304 | "2022-11-15 01:30:00", 305 | "2022-11-16 01:30:00", 306 | "2022-11-17 01:30:00", 307 | "2022-11-18 01:30:00", 308 | "2022-11-19 01:30:00", 309 | "2022-11-22 01:30:00", 310 | "2022-11-23 01:30:00", 311 | "2022-11-24 01:30:00", 312 | ], 313 | tz=tz_oz, 314 | ) 315 | dti_oz = dti_utc.tz_convert(tz_oz) 316 | assert_index_equal(dti_oz, expected_oz) 317 | expected_oz_days = pd.Index( 318 | [ 319 | datetime.date(2022, 11, 5), 320 | datetime.date(2022, 11, 8), 321 | datetime.date(2022, 11, 9), 322 | datetime.date(2022, 11, 10), 323 | datetime.date(2022, 11, 11), 324 | datetime.date(2022, 11, 12), 325 | datetime.date(2022, 11, 15), 326 | datetime.date(2022, 11, 16), 327 | datetime.date(2022, 11, 17), 328 | datetime.date(2022, 11, 18), 329 | datetime.date(2022, 11, 19), 330 | datetime.date(2022, 11, 22), 331 | datetime.date(2022, 11, 23), 332 | datetime.date(2022, 11, 24), 333 | ] 334 | ) 335 | assert_index_equal(pd.Index(dti_oz.date), expected_oz_days) 336 | 337 | expected_us = pd.DatetimeIndex( 338 | [ 339 | "2022-11-04 09:30:00", 340 | "2022-11-07 09:30:00", 341 | "2022-11-08 09:30:00", 342 | "2022-11-09 09:30:00", 343 | "2022-11-10 09:30:00", 344 | "2022-11-11 09:30:00", 345 | "2022-11-14 09:30:00", 346 | "2022-11-15 09:30:00", 347 | "2022-11-16 09:30:00", 348 | "2022-11-17 09:30:00", 349 | "2022-11-18 09:30:00", 350 | "2022-11-21 09:30:00", 351 | "2022-11-22 09:30:00", 352 | "2022-11-23 09:30:00", 353 | ], 354 | tz=tz_us, 355 | ) 356 | dti_us = dti_utc.tz_convert(tz_us) 357 | assert_index_equal(dti_us, expected_us) 358 | expected_us_days = pd.Index( 359 | [ 360 | datetime.date(2022, 11, 4), 361 | datetime.date(2022, 11, 7), 362 | datetime.date(2022, 11, 8), 363 | datetime.date(2022, 11, 9), 364 | datetime.date(2022, 11, 10), 365 | datetime.date(2022, 11, 11), 366 | datetime.date(2022, 11, 14), 367 | datetime.date(2022, 11, 15), 368 | datetime.date(2022, 11, 16), 369 | datetime.date(2022, 11, 17), 370 | datetime.date(2022, 11, 18), 371 | datetime.date(2022, 11, 21), 372 | datetime.date(2022, 11, 22), 373 | datetime.date(2022, 11, 23), 374 | ] 375 | ) 376 | assert_index_equal(pd.Index(dti_us.date), expected_us_days) 377 | 378 | expected_hk = pd.DatetimeIndex( 379 | [ 380 | "2022-11-04 21:30", 381 | "2022-11-07 22:30", 382 | "2022-11-08 22:30", 383 | "2022-11-09 22:30", 384 | "2022-11-10 22:30", 385 | "2022-11-11 22:30", 386 | "2022-11-14 22:30", 387 | "2022-11-15 22:30", 388 | "2022-11-16 22:30", 389 | "2022-11-17 22:30", 390 | "2022-11-18 22:30", 391 | "2022-11-21 22:30", 392 | "2022-11-22 22:30", 393 | "2022-11-23 22:30", 394 | ], 395 | tz=tz_hk, 396 | ) 397 | dti_hk = dti_utc.tz_convert(tz_hk) 398 | assert_index_equal(dti_hk, expected_hk) 399 | expected_hk_days = expected_oz_days # same, both should map to next day 400 | assert_index_equal( 401 | pd.Index(dti_hk.date + datetime.timedelta(1)), expected_hk_days 402 | ) 403 | 404 | yield ( 405 | tss, 406 | expected_us_days, 407 | expected_oz_days, 408 | expected_hk_days, 409 | expected_utc_days, 410 | ) 411 | 412 | @pytest.fixture 413 | def quote(self): 414 | """Fictional mock OHLCV data for 14 datapoints. 415 | 416 | Yields both unordered data and dictionary representing expected 417 | order of return. 418 | """ 419 | opens = list(range(2, 16)) 420 | lows = list(range(1, 15)) 421 | highs = list(range(4, 18)) 422 | closes = list(range(3, 17)) 423 | volumes = list(range(50, 64)) 424 | data = { 425 | "volume": volumes, 426 | "close": closes, 427 | "open": opens, 428 | "high": highs, 429 | "low": lows, 430 | } 431 | expected = { 432 | "open": opens, 433 | "high": highs, 434 | "low": lows, 435 | "close": closes, 436 | "volume": volumes, 437 | } 438 | yield data, expected 439 | 440 | @pytest.fixture 441 | def adjclose(self): 442 | """Fictional mock adjclose data for 14 datapoints.""" 443 | yield [i + 0.25 for i in range(3, 17)] 444 | 445 | @staticmethod 446 | def get_dividends(tss): 447 | """Get fictional mock dividends data for 2 timestamps of `tss`. 448 | 449 | Returns 450 | ------- 451 | tuple[dict[str, dict[str, float | int]], list[float]] 452 | [0] dict[str, dict[str, float | int]] 453 | Mock data for symbol_data["events"]["dividends"]. Data 454 | includes dividends for two timestamps of `tss`. 455 | [1] list[float] 456 | Expected contents of dividends column of DataFrame created 457 | for `tss` and with data that includes [0]. 458 | """ 459 | indices = (2, 8) 460 | amount = 0.12 461 | d = {str(tss[i]): {"amount": amount, "date": tss[i]} for i in indices} 462 | expected = [amount if i in indices else float("nan") for i in range(14)] 463 | return d, expected 464 | 465 | @pytest.fixture 466 | def dividends_daily(self, timestamps_daily): 467 | """Mock data and expected col values for daily dividends. 468 | 469 | See `get_dividends.__doc__` 470 | """ 471 | yield self.get_dividends(timestamps_daily[0]) 472 | 473 | @staticmethod 474 | def get_splits(tss): 475 | """Get fictional mock splits data for 1 timestamps of `tss`. 476 | 477 | Returns 478 | ------- 479 | tuple[dict[str, dict[str, int | str]], list[float]] 480 | [0] dict[str, dict[str, float | int]] 481 | Mock data for symbol_data["events"]["splits"]. Data 482 | includes splits for one timestamp of `tss`. 483 | [1] list[float] 484 | Expected contents of splits column of DataFrame created 485 | for `tss` and with data that includes [0]. 486 | """ 487 | indice = 11 488 | ts = tss[indice] 489 | d = { 490 | str(ts): {"data": ts, "numerator": 3, "denominator": 1, "splitRatio": "3:1"} 491 | } 492 | expected = [3 if i == indice else float("nan") for i in range(14)] 493 | return d, expected 494 | 495 | @pytest.fixture 496 | def splits_daily(self, timestamps_daily): 497 | """Mock data and expected col values for daily splits. 498 | 499 | See `get_splits.__doc__` 500 | """ 501 | yield self.get_splits(timestamps_daily[0]) 502 | 503 | @staticmethod 504 | def build_mock_data( 505 | tss, tz, quote, adjclose=None, splits=None, dividends=None, last_trade=None 506 | ): 507 | """Get mock data for a symbol from which to create dataframe. 508 | 509 | Return can be passed as `data` parameter of `history_dataframe`. 510 | """ 511 | if last_trade is None: 512 | last_trade = 1669237204 513 | expected_ts = pd.Timestamp("2022-11-23 21:00:04", tz="UTC") 514 | assert pd.Timestamp.fromtimestamp(last_trade, tz="UTC") == expected_ts 515 | meta = { 516 | "regularMarketTime": last_trade, 517 | "exchangeTimezoneName": tz, 518 | } 519 | 520 | indicators = {"quote": [quote.copy()]} 521 | if adjclose is not None: 522 | indicators["adjclose"] = [{"adjclose": adjclose}] 523 | 524 | events = {"fake_event": {"1667568600": {"fake_event_key": 66.6}}} 525 | for key, event_data in zip(("dividends", "splits"), (dividends, splits)): 526 | if event_data is None: 527 | continue 528 | events[key] = event_data 529 | 530 | return dict(meta=meta, indicators=indicators, timestamp=tss, events=events) 531 | 532 | @staticmethod 533 | def create_expected(expected_index, quote, dividends, splits, adjclose=None): 534 | """Create expected return from column parts.""" 535 | df = pd.DataFrame(quote, index=expected_index) 536 | if adjclose is not None: 537 | df["adjclose"] = adjclose 538 | df["dividends"] = dividends 539 | df["splits"] = splits 540 | return df 541 | 542 | @staticmethod 543 | def verify_expected_daily_row_11(df, indice): 544 | """Hard coded sanity check on specific row of expected dataframe.""" 545 | i = 11 546 | expected = pd.Series( 547 | dict(open=13, high=15, low=12, close=14, volume=61, adjclose=14.25), 548 | name=indice, 549 | ) 550 | assert_series_equal(df.iloc[i].iloc[:-2], expected) 551 | assert pd.isna(df.iloc[i].iloc[-2]) # no dividends 552 | assert df.iloc[i].iloc[-1] == 3 # splits 553 | return df 554 | 555 | @pytest.fixture 556 | def expected_daily_utc( 557 | self, timestamps_daily, quote, dividends_daily, splits_daily, adjclose 558 | ): 559 | """Expected return if timestamps interpreted with local tz as utc.""" 560 | df = self.create_expected( 561 | timestamps_daily[4], quote[1], dividends_daily[1], splits_daily[1], adjclose 562 | ) 563 | self.verify_expected_daily_row_11(df, datetime.date(2022, 11, 22)) 564 | yield df 565 | 566 | @pytest.fixture 567 | def expected_daily_us( 568 | self, timestamps_daily, quote, dividends_daily, splits_daily, adjclose 569 | ): 570 | """Expected return if timestamps interpreted with local tz as us.""" 571 | df = self.create_expected( 572 | timestamps_daily[1], quote[1], dividends_daily[1], splits_daily[1], adjclose 573 | ) 574 | self.verify_expected_daily_row_11(df, datetime.date(2022, 11, 21)) 575 | yield df 576 | 577 | @pytest.fixture 578 | def expected_daily_us_bare(self, timestamps_daily, quote): 579 | """As `expected_daily_us` with only ohlcv columns.""" 580 | df = pd.DataFrame(quote[1], index=timestamps_daily[1]) 581 | # Hard coded sanity check for specific row 582 | i = 11 583 | expected = pd.Series( 584 | dict(open=13, high=15, low=12, close=14, volume=61), 585 | name=datetime.date(2022, 11, 21), 586 | ) 587 | assert_series_equal(df.iloc[i], expected) 588 | yield df 589 | 590 | @pytest.fixture 591 | def expected_daily_oz( 592 | self, timestamps_daily, quote, dividends_daily, splits_daily, adjclose 593 | ): 594 | """Expected return if timestamps interpreted with local tz as oz.""" 595 | df = self.create_expected( 596 | timestamps_daily[2], quote[1], dividends_daily[1], splits_daily[1], adjclose 597 | ) 598 | self.verify_expected_daily_row_11(df, datetime.date(2022, 11, 22)) 599 | yield df 600 | 601 | @pytest.fixture 602 | def expected_daily_hk( 603 | self, timestamps_daily, quote, dividends_daily, splits_daily, adjclose 604 | ): 605 | """Expected return if timestamps interpreted with local tz as oz.""" 606 | df = self.create_expected( 607 | timestamps_daily[3], quote[1], dividends_daily[1], splits_daily[1], adjclose 608 | ) 609 | self.verify_expected_daily_row_11(df, datetime.date(2022, 11, 22)) 610 | yield df 611 | 612 | def test_daily( 613 | self, 614 | timestamps_daily, 615 | quote, 616 | adjclose, 617 | dividends_daily, 618 | splits_daily, 619 | expected_daily_utc, 620 | expected_daily_us, 621 | expected_daily_oz, 622 | expected_daily_hk, 623 | utc, 624 | tz_us, 625 | tz_oz, 626 | tz_hk, 627 | ): 628 | """Test for expected returns for mock data reflecting a daily period.""" 629 | 630 | def f(data, adj_timezone): 631 | return history_dataframe(data, daily=True, adj_timezone=adj_timezone) 632 | 633 | tss = timestamps_daily[0] 634 | quote_, _ = quote 635 | adjclose_ = adjclose 636 | splits, _ = splits_daily 637 | dividends, _ = dividends_daily 638 | 639 | expecteds = ( 640 | expected_daily_utc, 641 | expected_daily_us, 642 | expected_daily_oz, 643 | expected_daily_hk, 644 | ) 645 | tzs = (utc, tz_us, tz_oz, tz_hk) 646 | for expected, tz in zip(expecteds, tzs): 647 | data = self.build_mock_data(tss, tz, quote_, adjclose_, splits, dividends) 648 | for adj_timezone in (True, False): 649 | # tz makes no difference as daily and there is no live indice 650 | rtrn = f(data, adj_timezone=adj_timezone) 651 | assert_frame_equal(rtrn, expected) 652 | 653 | # check effect if there are no dividends and/or splits 654 | expected = expected_daily_us 655 | tz = tz_us 656 | adj_timezone = False 657 | # no dividends 658 | dividends_srs = expected.pop("dividends") 659 | data = self.build_mock_data(tss, tz, quote_, adjclose_, splits=splits) 660 | rtrn = f(data, adj_timezone) 661 | assert_frame_equal(rtrn, expected) 662 | # no splits 663 | expected.pop("splits") 664 | expected["dividends"] = dividends_srs 665 | data = self.build_mock_data(tss, tz, quote_, adjclose_, dividends=dividends) 666 | rtrn = f(data, adj_timezone) 667 | assert_frame_equal(rtrn, expected) 668 | # neither dividends nor splits 669 | expected.pop("dividends") 670 | data = self.build_mock_data(tss, tz, quote_, adjclose_) 671 | rtrn = f(data, adj_timezone) 672 | assert_frame_equal(rtrn, expected) 673 | 674 | def test_live_indice( 675 | self, timestamps_daily, expected_daily_us_bare, tz_us, utc, quote 676 | ): 677 | """Test daily data with live indice.""" 678 | live_indice = 1669231860 679 | expected_li_ts = pd.Timestamp("2022-11-23 19:31", tz="UTC") 680 | assert pd.Timestamp.fromtimestamp(live_indice, tz="UTC") == expected_li_ts 681 | 682 | tss, expected_days, *_ = timestamps_daily 683 | tss = tss[:-1] 684 | tss.append(live_indice) 685 | 686 | expected_df = expected_daily_us_bare 687 | data = self.build_mock_data(tss, tz_us, quote[0], last_trade=live_indice) 688 | 689 | # verify live indice has utc timezone when adj_timezone True 690 | rtrn = history_dataframe(data, daily=True, adj_timezone=False) 691 | expected_li = pd.Timestamp("2022-11-23 19:31", tz=utc).to_pydatetime() 692 | expected_index = expected_days[:-1] 693 | expected_index = expected_index.insert(len(expected_index), expected_li) 694 | expected_df.index = expected_index 695 | assert_frame_equal(rtrn, expected_df) 696 | 697 | # verify live indice has local timezone when adj_timezone True 698 | rtrn = history_dataframe(data, daily=True, adj_timezone=True) 699 | expected_li = pd.Timestamp("2022-11-23 14:31", tz=tz_us).to_pydatetime() 700 | expected_index = expected_index[:-1].insert( 701 | len(expected_index) - 1, expected_li 702 | ) 703 | expected_df.index = expected_index 704 | assert_frame_equal(rtrn, expected_df) 705 | 706 | def test_duplicate_live_indice( 707 | self, timestamps_daily, expected_daily_us_bare, tz_us, quote 708 | ): 709 | """Test live indice removed if day already represented.""" 710 | live_indice = 1669237204 711 | expected_li_ts = pd.Timestamp("2022-11-23 21:00:04", tz="UTC") 712 | assert pd.Timestamp.fromtimestamp(live_indice, tz="UTC") == expected_li_ts 713 | 714 | tss = timestamps_daily[0] 715 | # to get it all to fit to 14 indices, lose the first ts 716 | tss = tss[1:] 717 | tss.append(live_indice) 718 | 719 | data = self.build_mock_data(tss, tz_us, quote[0], last_trade=live_indice) 720 | rtrn = history_dataframe(data, daily=True, adj_timezone=False) 721 | 722 | # create expected 723 | expected_template = expected_daily_us_bare 724 | expected_index = expected_template.index[1:] 725 | assert expected_index[-1] == datetime.date(2022, 11, 23) 726 | # last row, live indice, expected to be removed as day already represented 727 | expected_df = expected_template[:-1] 728 | expected_df.index = expected_index 729 | assert_frame_equal(rtrn, expected_df) 730 | 731 | @pytest.fixture 732 | def timestamps_intraday(self, utc): 733 | """Timestamps representing fictional datetimes and expected mapped indices. 734 | 735 | Timestamps cover two days with change in DST observance. 736 | 737 | Yields 738 | ------- 739 | tuple[list[int] 740 | [0] [list[int]] 741 | Unix timestamps, i.e. format as used by Yahoo API. Timestamps represent 742 | datetimes of hourly indices in terms of UTC. 743 | 744 | [1] pd.DatetimeIndex dtype 'datetime64[ns, UTC]' 745 | Expected indices that timestamps would map to if local timezone were 746 | 'UTC'. 747 | """ 748 | tss = [ 749 | 1667568600, 750 | 1667572200, 751 | 1667575800, 752 | 1667579400, 753 | 1667583000, 754 | 1667586600, 755 | 1667590200, 756 | 1667831400, 757 | 1667835000, 758 | 1667838600, 759 | 1667842200, 760 | 1667845800, 761 | 1667849400, 762 | 1667853000, 763 | ] 764 | 765 | expected_index_utc = pd.DatetimeIndex( 766 | [ 767 | "2022-11-04 13:30:00", 768 | "2022-11-04 14:30:00", 769 | "2022-11-04 15:30:00", 770 | "2022-11-04 16:30:00", 771 | "2022-11-04 17:30:00", 772 | "2022-11-04 18:30:00", 773 | "2022-11-04 19:30:00", 774 | "2022-11-07 14:30:00", 775 | "2022-11-07 15:30:00", 776 | "2022-11-07 16:30:00", 777 | "2022-11-07 17:30:00", 778 | "2022-11-07 18:30:00", 779 | "2022-11-07 19:30:00", 780 | "2022-11-07 20:30:00", 781 | ], 782 | tz=utc, 783 | ) 784 | dti = pd.to_datetime(tss, unit="s") 785 | dti_utc = dti.tz_localize(utc) 786 | assert_index_equal(dti_utc, expected_index_utc) 787 | 788 | yield tss, expected_index_utc 789 | 790 | @pytest.fixture 791 | def dividends_intraday(self, timestamps_intraday): 792 | """Get mock data and expected col values for intraday dividends. 793 | 794 | The Yahoo API attaches any dividends to the first intraday indice 795 | of each session. This mock does not respect this alignment, which 796 | is inconsequential for the test purposes. 797 | 798 | See `get_dividends.__doc__`. 799 | """ 800 | yield self.get_dividends(timestamps_intraday[0]) 801 | 802 | @pytest.fixture 803 | def splits_intraday(self, timestamps_intraday): 804 | """Mock data and expected col values for intraday splits. 805 | 806 | The Yahoo API attaches any dividends to the first intraday indice 807 | of each session. This mock does not respect this alignment, which 808 | is inconsequential for the test purposes. 809 | 810 | See `get_splits.__doc__`. 811 | """ 812 | yield self.get_splits(timestamps_intraday[0]) 813 | 814 | @pytest.fixture 815 | def expected_intraday( 816 | self, timestamps_intraday, quote, dividends_intraday, splits_intraday 817 | ): 818 | """Expected return for intraday timestamps.""" 819 | _, expected_utc = timestamps_intraday 820 | df = self.create_expected( 821 | expected_utc, quote[1], dividends_intraday[1], splits_intraday[1] 822 | ) 823 | # hard coded sanity check on specific row 824 | i = 8 825 | expected = pd.Series( 826 | dict(open=10, high=12, low=9, close=11, volume=58, dividends=0.12), 827 | name=pd.Timestamp("2022-11-7 15:30", tz="UTC"), 828 | ) 829 | assert_series_equal(df.iloc[i][:-1], expected) 830 | series = df.iloc[i] 831 | assert pd.isna(series.iloc[-1]) 832 | yield df 833 | 834 | def test_intraday( 835 | self, 836 | timestamps_intraday, 837 | tz_us, 838 | quote, 839 | splits_intraday, 840 | dividends_intraday, 841 | expected_intraday, 842 | ): 843 | """Test for expected returns for mock data reflecting a daily period.""" 844 | 845 | def f(data, adj_timezone): 846 | return history_dataframe(data, daily=False, adj_timezone=adj_timezone) 847 | 848 | tz = tz_us 849 | tss, _ = timestamps_intraday 850 | quote_, _ = quote 851 | splits, _ = splits_intraday 852 | dividends, _ = dividends_intraday 853 | 854 | data = self.build_mock_data(tss, tz, quote_, splits=splits, dividends=dividends) 855 | rtrn = f(data, adj_timezone=False) 856 | expected = expected_intraday 857 | assert_frame_equal(rtrn, expected) 858 | rtrn = f(data, adj_timezone=True) 859 | expected.index = expected.index.tz_convert(tz) 860 | assert_frame_equal(rtrn, expected) 861 | 862 | # no dividends 863 | dividends_srs = expected.pop("dividends") 864 | data = self.build_mock_data(tss, tz, quote_, splits=splits) 865 | rtrn = f(data, adj_timezone=True) 866 | assert_frame_equal(rtrn, expected) 867 | # no splits 868 | expected.pop("splits") 869 | expected["dividends"] = dividends_srs 870 | data = self.build_mock_data(tss, tz, quote_, dividends=dividends) 871 | rtrn = f(data, adj_timezone=True) 872 | assert_frame_equal(rtrn, expected) 873 | # neither dividends nor splits 874 | expected.pop("dividends") 875 | data = self.build_mock_data(tss, tz, quote_) 876 | rtrn = f(data, adj_timezone=True) 877 | assert_frame_equal(rtrn, expected) 878 | -------------------------------------------------------------------------------- /docs/docs/guide/ticker/miscellaneous.md: -------------------------------------------------------------------------------- 1 | 2 | ### **corporate_events** 3 | 4 | === "Details" 5 | 6 | - *Description*: Significant events related to a given symbol(s) 7 | - *Return*: `pandas.DataFrame` 8 | 9 | === "Example" 10 | 11 | ```python hl_lines="2" 12 | aapl = Ticker('aapl') 13 | df = aapl.corporate_events 14 | df.head() 15 | ``` 16 | 17 | === "Data" 18 | 19 | | | date | significance | headline | parentTopics | 20 | |---:|:--------------------|---------------:|:--------------------------------------------------------------------------------------------------------------|:-------------------| 21 | | 0 | 2012-03-19 00:00:00 | 1 | Apple Inc. Announces Plans To Initiate Dividend And Share Repurchase Program | Performance | 22 | | 1 | 2012-10-25 00:00:00 | 3 | Apple Inc Declares Cash Dividend | Performance | 23 | | 2 | 2013-01-23 00:00:00 | 1 | Apple Inc Issues Q2 2013 Revenue Guidance Below Analysts' Estimates; Declares Cash Dividend | Corporate Guidance | 24 | | 3 | 2013-04-23 00:00:00 | 1 | Apple Inc Increases Repurchase Authorization To $60 Billion From The $10 Billion; Approves Quarterly Dividend | Ownership/Control | 25 | | 4 | 2013-07-23 00:00:00 | 3 | Apple Inc Declares Cash Dividend | Performance | 26 | 27 | ### **news** 28 | 29 | === "Details" 30 | 31 | - *Description*: Get news headline and summary information for given symbol(s) 32 | - *Return*: `list` 33 | - *Arguments*: 34 | 35 | | Argument | Type | Default | Required | Options | 36 | |:-----------|:-------|:----------|:-----------|:------------------------------| 37 | | count | `int` | `25` | optional | | 38 | | start | `str` or `datetime.datetime` | `None` | optional | If a `str` is used, the format should be YYYY-MM-DD | 39 | 40 | !!! warning 41 | It's recommended to use one symbol when utilizing this method as there's no discernible way to group resulting news items to the symbols they belong to. 42 | 43 | !!! tip 44 | If multiple symbols are used, only one request will be made regardless of the number of symbols 45 | 46 | === "Example" 47 | 48 | ```python hl_lines="2" 49 | aapl = Ticker('aapl') 50 | aapl.news(5) 51 | ``` 52 | 53 | === "Data" 54 | 55 | ```python 56 | [{ 57 | 'rank': 0, 58 | 'id': '3a3b4532-38fb-3fcb-85b3-3cdc4d5bbabe', 59 | 'tag': 'news', 60 | 'title': 'Big Tech’s CEOs Got the Last Word. Here’s Why Tech Earnings Were So Important.', 61 | 'summary': 'Amazon’s sales were up 40% in the June quarter, which one analyst referred to as shocking levels of growth. Meanwhile, Apple couldn’t keep up with demand for Macs and iPads.', 62 | 'url': 'https://www.barrons.com/articles/heres-why-amazon-apple-and-facebook-earnings-were-so-important-51596232561?siteid=yhoof2&yptr=yahoo', 63 | 'author_name': 'Eric J. Savitz', 64 | 'provider_publish_time': 1596232560, 65 | 'provider_name': 'Barrons.com', 66 | 'hosted': False, 67 | 'tickers': [], 68 | 'featured': False, 69 | 'timeZoneShortName': 'EDT', 70 | 'timeZoneFullName': 'America/New_York', 71 | 'gmtOffSetMilliseconds': -14400000, 72 | 'imageSet': {} 73 | }, { 74 | 'rank': 1, 75 | 'id': 'e0ea15cf-9cd4-33fa-b8ef-87ea7ee40c32', 76 | 'tag': 'news', 77 | 'title': 'Microsoft Is in Talks to Buy TikTok in U.S.', 78 | 'summary': '(Bloomberg) -- Microsoft Corp. is exploring an acquisition of TikTok’s operations in the U.S., according to a people familiar with the matter. A deal would give the software company a popular social-media service and relieve U.S. government pressure on the Chinese owner of the video-sharing app.The Trump administration has been weighing whether to direct China-based ByteDance Ltd. to divest its stake in TikTok’s U.S. operations, according to several people familiar with the issue. The U.S. has been investigating potential national security risks due to the Chinese company’s control of the app.While the administration was prepared to announce an order as soon as Friday, according to three people familiar with the matter, another person said later that the decision was on hold, pending further review by President Donald Trump. All of the people asked not to be identified because the deliberations are private.Spokespeople for Microsoft and TikTok declined to comment on any potential talks. The software company’s interest in the app was reported earlier by Fox Business Network.“We are looking at TikTok. We may be banning TikTok,” Trump told reporters Friday at the White House. “We are looking at a lot of alternatives with respect to TikTok.”Any transaction could face regulatory hurdles. ByteDance bought Musical.ly Inc. in 2017 and merged it with TikTok, creating a social-media hit in the U.S -- the first Chinese app to make such inroads. As TikTok became more popular, U.S. officials grew concerned about the potential for the Chinese government to use the app to gain data on U.S. citizens.The Committee on Foreign Investment in the U.S. began a review in 2019 of the Musical.ly purchase. In recent years, CFIUS, which investigates overseas acquisitions of U.S. businesses, has taken a much more aggressive role in reviewing and approving deals that may threaten national security. It can recommend that the president block or unwind transactions.It’s also possible that other potential buyers could come forward, said another person familiar with the discussions. Microsoft’s industry peers -- Facebook Inc., Apple Inc., Amazon.com Inc. and Alphabet Inc. -- fit the profile of potential suitors, though all are under antitrust scrutiny from U.S. regulators, which would likely complicate a deal.A purchase of TikTok would represent a huge coup for Microsoft, which would gain a popular consumer app that has won over young people with a steady diet of dance videos, lip-syncing clips and viral memes. The company has dabbled in social-media investments in the past, but hasn’t developed a popular service of its own in the lucrative sector. Microsoft acquired the LinkedIn job-hunting and corporate networking company for $26.2 billion in 2016.Microsoft can point to one acquisition that came with a massive existing community of users that has increased under its ownership -- the 2014 deal for Minecraft, the best-selling video game ever.Other purchases of popular services have gone less well. The 2011 pickup of Skype led to several years of stagnation for the voice-calling service and Microsoft fell behind newer products in the category. Outside of Xbox, the company hasn’t focused on younger consumers. A TikTok deal could change that, though, and give Microsoft “a crown jewel on the consumer social media front,” Dan Ives, an analyst at Wedbush Securities, wrote in a note to investors Friday.TikTok has repeatedly rejected accusations that it feeds user data to China or is beholden to Beijing, even though ByteDance is based there. TikTok now has a U.S.-based chief executive officer and ByteDance has considered making other organizational changes to satisfy U.S. authorities.“Hundreds of millions of people come to TikTok for entertainment and connection, including our community of creators and artists who are building livelihoods from the platform,” a TikTok spokeswoman said Friday. “We’re motivated by their passion and creativity, and committed to protecting their privacy and safety as we continue working to bring joy to families and meaningful careers to those who create on our platform.”The mechanics of separating the TikTok app in the U.S. from the rest of its operations won’t come without complications. Unlike many tech companies in the U.S. where engineers for, say, Google, work on particular products like YouTube or Google Maps, many of ByteDance’s engineers work across its different platforms and services and continue to work on TikTok globally.On Thursday, U.S. Senators Josh Hawley, a Missouri Republican, and Richard Blumenthal, a Connecticut Democrat, wrote the Justice Department asking for an investigation of whether TikTok has violated the constitutional rights of Americans by sharing private information with the Chinese government.A deal with Microsoft could potentially help extract ByteDance from the political war between the U.S. and China.U.S. Senator Marco Rubio, a Florida Republican and member of the Senate’s Select Committee on Intelligence, applauded the idea of a TikTok sale. “In its current form, TikTok represents a potential threat to personal privacy and our national security,” Rubio said in a statement. “We must do more than simply remove ByteDance from the equation. Moving forward, we must establish a framework of standards that must be met before a high-risk, foreign-based app is allowed to operate on American telecommunications networks and devices.”(Updates with details of TikTok’s operations in the 14th paragraph.)For more articles like this, please visit us at bloomberg.comSubscribe now to stay ahead with the most trusted business news source.©2020 Bloomberg L.P.', 79 | 'url': 'https://finance.yahoo.com/news/microsoft-said-talks-buy-tiktok-185221680.html', 80 | 'author_name': 'Kurt Wagner, Jennifer Jacobs, Saleha Mohsin and Jenny Leonard', 81 | 'provider_publish_time': 1596232267, 82 | 'provider_name': 'Bloomberg', 83 | 'hosted': True, 84 | 'tickers': [], 85 | 'thumbnail': 'https://media.zenfs.com/en/bloomberg_technology_68/d998c0ffe5e9a652dce2e465c5d6d8a3', 86 | 'featured': False, 87 | 'timeZoneShortName': 'EDT', 88 | 'timeZoneFullName': 'America/New_York', 89 | 'gmtOffSetMilliseconds': -14400000, 90 | 'imageSet': {} 91 | }, { 92 | 'rank': 2, 93 | 'id': '8cc7e7d9-87ec-3aaf-8a51-24f8ae1f6eca', 94 | 'tag': 'news', 95 | 'title': 'Bitcoin Is Rising Again. Asking Why Takes You Down the Financial Rabbit Hole.', 96 | 'summary': 'How to explain Bitcoin’s reversal of fortune? In search of an answer our columnist explores the dollar, the euro, gold, stocks, inflation, and more.', 97 | 'url': 'https://www.barrons.com/articles/bitcoin-is-rising-again-asking-why-takes-you-down-a-financial-rabbit-hole-51596231804?siteid=yhoof2&yptr=yahoo', 98 | 'author_name': 'Jack Hough', 99 | 'provider_publish_time': 1596231780, 100 | 'provider_name': 'Barrons.com', 101 | 'hosted': False, 102 | 'tickers': [], 103 | 'featured': False, 104 | 'timeZoneShortName': 'EDT', 105 | 'timeZoneFullName': 'America/New_York', 106 | 'gmtOffSetMilliseconds': -14400000, 107 | 'imageSet': {} 108 | }, { 109 | 'rank': 3, 110 | 'id': '14ba64e7-a900-3e30-b513-94a5efce0dd7', 111 | 'tag': 'news', 112 | 'title': 'Apple Stock Rallied 10% Friday. Here’s Why.', 113 | 'summary': 'One day after reporting June quarter results, Apple (ticker: AAPL) stock surged 10.5% to $425.04, the stock’s first-ever close above $400. The stock won’t stay there for long, though: The company Thursday declared a 4-for-1 stock split, effective August 31. A big revenue beat: Apple had $59.7 billion in sales in the quarter, up 11% from a year ago, and beating consensus by $7.6 billion.', 114 | 'url': 'https://www.barrons.com/articles/apple-stock-hits-new-highs-earnings-51596215820?siteid=yhoof2&yptr=yahoo', 115 | 'author_name': 'Eric J. Savitz', 116 | 'provider_publish_time': 1596231300, 117 | 'provider_name': 'Barrons.com', 118 | 'hosted': False, 119 | 'tickers': [], 120 | 'featured': False, 121 | 'timeZoneShortName': 'EDT', 122 | 'timeZoneFullName': 'America/New_York', 123 | 'gmtOffSetMilliseconds': -14400000, 124 | 'imageSet': {} 125 | }, { 126 | 'rank': 4, 127 | 'id': '5894f228-5bad-3eda-8a48-d8fc0bdced0c', 128 | 'tag': 'news', 129 | 'title': 'Apple Tops Saudi Aramco as World’s Most Valuable Company', 130 | 'summary': '(Bloomberg) -- Apple Inc. became the world’s most valuable company with its market value overtaking Saudi Aramco in the wake of better-than-expected earnings.Apple jumped 10% on Friday, ending the day with a record market capitalization of $1.817 trillion. It’s the first time the company’s valuation has surpassed that of Saudi Arabia’s national oil company, which made its market debut in Riyadh in December, and is valued at $1.76 trillion. Before that, Apple had vied with Microsoft Corp. for the title of the U.S.’s largest public company.The dethroning of Aramco comes after a tumultuous period for the Saudi company. Its initial public offering fell short of Crown Price Mohammed bin Salman’s expectations. The kingdom’s de facto ruler initially wanted a valuation of $2 trillion and to raise $100 billion. But after foreign investors balked at the pricing, the government settled on a smaller domestic offering and raised about $30 billion, still the largest IPO ever.Then came this year’s plunge in crude prices as energy demand crashed with the spread of the virus. Aramco’s second-quarter revenue probably dropped to about $37 billion from $76 billion a year earlier, according to analyst estimates compiled by Bloomberg. That’s less than the $59.7 billion in sales that Apple reported for its most recent period.Aramco’s stock is down 6.4% since the end of December, though that’s far less than the fall of other oil majors. Exxon Mobil Corp. has declined 40% and Royal Dutch Shell Plc has dropped 50%.Apple, meanwhile, has benefited as the pandemic has strengthened the market positions of the world’s biggest technology companies, which boast strong balance sheets and fast-growing businesses thanks to an acceleration in the shift to digital services. The iPhone maker’s shares have gained 45% so far this year.(Adds closing share values throughout. A previous version of this story corrected the market cap gain.)For more articles like this, please visit us at bloomberg.comSubscribe now to stay ahead with the most trusted business news source.©2020 Bloomberg L.P.', 131 | 'url': 'https://finance.yahoo.com/news/apple-briefly-tops-saudi-aramco-153503708.html', 132 | 'author_name': 'Jeran Wittenstein and Matthew Martin', 133 | 'provider_publish_time': 1596229879, 134 | 'provider_name': 'Bloomberg', 135 | 'hosted': True, 136 | 'tickers': [], 137 | 'thumbnail': 'https://media.zenfs.com/en/bloomberg_markets_842/8a17157f40b14e617bb6ed1b8e2774f5', 138 | 'featured': False, 139 | 'timeZoneShortName': 'EDT', 140 | 'timeZoneFullName': 'America/New_York', 141 | 'gmtOffSetMilliseconds': -14400000, 142 | 'imageSet': {} 143 | }] 144 | ``` 145 | 146 | ### **quotes** 147 | 148 | === "Details" 149 | 150 | - *Description*: Get real-time quote information for given symbol(s) 151 | - *Return*: `list` 152 | 153 | !!! tip 154 | If multiple symbols are used, only one request will be made regardless of the number of symbols 155 | 156 | === "Example" 157 | 158 | ```python hl_lines="2" 159 | tickers = Ticker('fb aapl amzn nflx goog') 160 | tickers.quotes 161 | ``` 162 | 163 | === "Data" 164 | 165 | ```python 166 | [{ 167 | 'language': 'en-US', 168 | 'region': 'US', 169 | 'quoteType': 'EQUITY', 170 | 'quoteSourceName': 'Nasdaq Real Time Price', 171 | 'triggerable': True, 172 | 'currency': 'USD', 173 | 'tradeable': False, 174 | 'firstTradeDateMilliseconds': 1337347800000, 175 | 'priceHint': 2, 176 | 'postMarketChangePercent': -0.067015484, 177 | 'postMarketTime': 1596233013, 178 | 'postMarketPrice': 253.5, 179 | 'postMarketChange': -0.16999817, 180 | 'regularMarketChange': 19.169998, 181 | 'regularMarketChangePercent': 8.17484, 182 | 'regularMarketTime': 1596225602, 183 | 'regularMarketPrice': 253.67, 184 | 'regularMarketDayHigh': 255.85, 185 | 'regularMarketDayRange': '249.0 - 255.85', 186 | 'regularMarketDayLow': 249.0, 187 | 'regularMarketVolume': 52105898, 188 | 'regularMarketPreviousClose': 234.5, 189 | 'bid': 253.31, 190 | 'ask': 253.7, 191 | 'bidSize': 11, 192 | 'askSize': 12, 193 | 'fullExchangeName': 'NasdaqGS', 194 | 'financialCurrency': 'USD', 195 | 'regularMarketOpen': 255.82, 196 | 'averageDailyVolume3Month': 24501318, 197 | 'averageDailyVolume10Day': 17021500, 198 | 'fiftyTwoWeekLowChange': 116.56999, 199 | 'fiftyTwoWeekLowChangePercent': 0.8502552, 200 | 'fiftyTwoWeekRange': '137.1 - 255.85', 201 | 'fiftyTwoWeekHighChange': -2.180008, 202 | 'fiftyTwoWeekHighChangePercent': -0.008520649, 203 | 'fiftyTwoWeekLow': 137.1, 204 | 'fiftyTwoWeekHigh': 255.85, 205 | 'earningsTimestamp': 1596124800, 206 | 'earningsTimestampStart': 1603868340, 207 | 'earningsTimestampEnd': 1604304000, 208 | 'trailingPE': 34.80653, 209 | 'epsTrailingTwelveMonths': 7.288, 210 | 'epsForward': 9.74, 211 | 'sharesOutstanding': 2408470016, 212 | 'bookValue': 36.936, 213 | 'fiftyDayAverage': 235.86743, 214 | 'fiftyDayAverageChange': 17.802567, 215 | 'fiftyDayAverageChangePercent': 0.075477004, 216 | 'twoHundredDayAverage': 207.2297, 217 | 'twoHundredDayAverageChange': 46.440292, 218 | 'twoHundredDayAverageChangePercent': 0.22410056, 219 | 'marketCap': 723725582336, 220 | 'forwardPE': 26.044147, 221 | 'priceToBook': 6.867825, 222 | 'sourceInterval': 15, 223 | 'exchangeDataDelayedBy': 0, 224 | 'marketState': 'POST', 225 | 'exchange': 'NMS', 226 | 'shortName': 'Facebook, Inc.', 227 | 'longName': 'Facebook, Inc.', 228 | 'messageBoardId': 'finmb_20765463', 229 | 'exchangeTimezoneName': 'America/New_York', 230 | 'exchangeTimezoneShortName': 'EDT', 231 | 'gmtOffSetMilliseconds': -14400000, 232 | 'market': 'us_market', 233 | 'esgPopulated': False, 234 | 'displayName': 'Facebook', 235 | 'symbol': 'FB' 236 | }, { 237 | 'language': 'en-US', 238 | 'region': 'US', 239 | 'quoteType': 'EQUITY', 240 | 'quoteSourceName': 'Delayed Quote', 241 | 'triggerable': True, 242 | 'currency': 'USD', 243 | 'tradeable': False, 244 | 'firstTradeDateMilliseconds': 345479400000, 245 | 'priceHint': 2, 246 | 'postMarketChangePercent': 0.519949, 247 | 'postMarketTime': 1596233031, 248 | 'postMarketPrice': 427.25, 249 | 'postMarketChange': 2.2099915, 250 | 'regularMarketChange': 40.28, 251 | 'regularMarketChangePercent': 10.468863, 252 | 'regularMarketTime': 1596225602, 253 | 'regularMarketPrice': 425.04, 254 | 'regularMarketDayHigh': 425.66, 255 | 'regularMarketDayRange': '403.36 - 425.66', 256 | 'regularMarketDayLow': 403.36, 257 | 'regularMarketVolume': 91201476, 258 | 'regularMarketPreviousClose': 384.76, 259 | 'bid': 427.26, 260 | 'ask': 426.2, 261 | 'bidSize': 30, 262 | 'askSize': 31, 263 | 'fullExchangeName': 'NasdaqGS', 264 | 'financialCurrency': 'USD', 265 | 'regularMarketOpen': 411.535, 266 | 'averageDailyVolume3Month': 34664412, 267 | 'averageDailyVolume10Day': 32670312, 268 | 'fiftyTwoWeekLowChange': 232.46, 269 | 'fiftyTwoWeekLowChangePercent': 1.2070827, 270 | 'fiftyTwoWeekRange': '192.58 - 425.66', 271 | 'fiftyTwoWeekHighChange': -0.6199951, 272 | 'fiftyTwoWeekHighChangePercent': -0.0014565501, 273 | 'fiftyTwoWeekLow': 192.58, 274 | 'fiftyTwoWeekHigh': 425.66, 275 | 'dividendDate': 1589414400, 276 | 'earningsTimestamp': 1596124800, 277 | 'earningsTimestampStart': 1603868340, 278 | 'earningsTimestampEnd': 1604304000, 279 | 'trailingAnnualDividendRate': 3.08, 280 | 'trailingPE': 32.236633, 281 | 'trailingAnnualDividendYield': 0.0080049895, 282 | 'epsTrailingTwelveMonths': 13.185, 283 | 'epsForward': 14.97, 284 | 'sharesOutstanding': 4334329856, 285 | 'bookValue': 16.761, 286 | 'fiftyDayAverage': 369.66028, 287 | 'fiftyDayAverageChange': 55.37973, 288 | 'fiftyDayAverageChangePercent': 0.1498125, 289 | 'twoHundredDayAverage': 313.8948, 290 | 'twoHundredDayAverageChange': 111.1452, 291 | 'twoHundredDayAverageChangePercent': 0.35408422, 292 | 'marketCap': 1842263621632, 293 | 'forwardPE': 28.392786, 294 | 'priceToBook': 25.35887, 295 | 'sourceInterval': 15, 296 | 'exchangeDataDelayedBy': 0, 297 | 'marketState': 'POST', 298 | 'exchange': 'NMS', 299 | 'shortName': 'Apple Inc.', 300 | 'longName': 'Apple Inc.', 301 | 'messageBoardId': 'finmb_24937', 302 | 'exchangeTimezoneName': 'America/New_York', 303 | 'exchangeTimezoneShortName': 'EDT', 304 | 'gmtOffSetMilliseconds': -14400000, 305 | 'market': 'us_market', 306 | 'esgPopulated': False, 307 | 'displayName': 'Apple', 308 | 'symbol': 'AAPL' 309 | }, { 310 | 'language': 'en-US', 311 | 'region': 'US', 312 | 'quoteType': 'EQUITY', 313 | 'quoteSourceName': 'Nasdaq Real Time Price', 314 | 'triggerable': True, 315 | 'currency': 'USD', 316 | 'tradeable': False, 317 | 'firstTradeDateMilliseconds': 863703000000, 318 | 'priceHint': 2, 319 | 'postMarketChangePercent': -0.2673244, 320 | 'postMarketTime': 1596232724, 321 | 'postMarketPrice': 3156.22, 322 | 'postMarketChange': -8.459961, 323 | 'regularMarketChange': 112.80005, 324 | 'regularMarketChangePercent': 3.696084, 325 | 'regularMarketTime': 1596225602, 326 | 'regularMarketPrice': 3164.68, 327 | 'regularMarketDayHigh': 3244.5, 328 | 'regularMarketDayRange': '3151.02 - 3244.5', 329 | 'regularMarketDayLow': 3151.02, 330 | 'regularMarketVolume': 7862360, 331 | 'regularMarketPreviousClose': 3051.88, 332 | 'bid': 3160.52, 333 | 'ask': 3160.0, 334 | 'bidSize': 8, 335 | 'askSize': 14, 336 | 'fullExchangeName': 'NasdaqGS', 337 | 'financialCurrency': 'USD', 338 | 'regularMarketOpen': 3244.0, 339 | 'averageDailyVolume3Month': 4675034, 340 | 'averageDailyVolume10Day': 4733650, 341 | 'fiftyTwoWeekLowChange': 1538.6499, 342 | 'fiftyTwoWeekLowChangePercent': 0.9462617, 343 | 'fiftyTwoWeekRange': '1626.03 - 3344.29', 344 | 'fiftyTwoWeekHighChange': -179.6101, 345 | 'fiftyTwoWeekHighChangePercent': -0.053706497, 346 | 'fiftyTwoWeekLow': 1626.03, 347 | 'fiftyTwoWeekHigh': 3344.29, 348 | 'earningsTimestamp': 1596124800, 349 | 'earningsTimestampStart': 1603382400, 350 | 'earningsTimestampEnd': 1603728000, 351 | 'trailingPE': 151.15971, 352 | 'epsTrailingTwelveMonths': 20.936, 353 | 'epsForward': 37.79, 354 | 'sharesOutstanding': 498776000, 355 | 'bookValue': 130.806, 356 | 'fiftyDayAverage': 2896.579, 357 | 'fiftyDayAverageChange': 268.10083, 358 | 'fiftyDayAverageChangePercent': 0.09255774, 359 | 'twoHundredDayAverage': 2323.4736, 360 | 'twoHundredDayAverageChange': 841.2063, 361 | 'twoHundredDayAverageChangePercent': 0.36204684, 362 | 'marketCap': 1578466410496, 363 | 'forwardPE': 83.74384, 364 | 'priceToBook': 24.193691, 365 | 'sourceInterval': 15, 366 | 'exchangeDataDelayedBy': 0, 367 | 'marketState': 'POST', 368 | 'exchange': 'NMS', 369 | 'shortName': 'Amazon.com, Inc.', 370 | 'longName': 'Amazon.com, Inc.', 371 | 'messageBoardId': 'finmb_18749', 372 | 'exchangeTimezoneName': 'America/New_York', 373 | 'exchangeTimezoneShortName': 'EDT', 374 | 'gmtOffSetMilliseconds': -14400000, 375 | 'market': 'us_market', 376 | 'esgPopulated': False, 377 | 'displayName': 'Amazon.com', 378 | 'symbol': 'AMZN' 379 | }, { 380 | 'language': 'en-US', 381 | 'region': 'US', 382 | 'quoteType': 'EQUITY', 383 | 'quoteSourceName': 'Nasdaq Real Time Price', 384 | 'triggerable': True, 385 | 'currency': 'USD', 386 | 'tradeable': False, 387 | 'firstTradeDateMilliseconds': 1022160600000, 388 | 'priceHint': 2, 389 | 'postMarketChangePercent': -0.5584215, 390 | 'postMarketTime': 1596232099, 391 | 'postMarketPrice': 486.15, 392 | 'postMarketChange': -2.730011, 393 | 'regularMarketChange': 3.080017, 394 | 'regularMarketChangePercent': 0.6340093, 395 | 'regularMarketTime': 1596225602, 396 | 'regularMarketPrice': 488.88, 397 | 'regularMarketDayHigh': 494.795, 398 | 'regularMarketDayRange': '484.5 - 494.795', 399 | 'regularMarketDayLow': 484.5, 400 | 'regularMarketVolume': 5797772, 401 | 'regularMarketPreviousClose': 485.8, 402 | 'bid': 487.3, 403 | 'ask': 488.56, 404 | 'bidSize': 9, 405 | 'askSize': 40, 406 | 'fullExchangeName': 'NasdaqGS', 407 | 'financialCurrency': 'USD', 408 | 'regularMarketOpen': 488.29, 409 | 'averageDailyVolume3Month': 7602690, 410 | 'averageDailyVolume10Day': 7325312, 411 | 'fiftyTwoWeekLowChange': 236.6, 412 | 'fiftyTwoWeekLowChangePercent': 0.93784684, 413 | 'fiftyTwoWeekRange': '252.28 - 575.37', 414 | 'fiftyTwoWeekHighChange': -86.48999, 415 | 'fiftyTwoWeekHighChangePercent': -0.15032065, 416 | 'fiftyTwoWeekLow': 252.28, 417 | 'fiftyTwoWeekHigh': 575.37, 418 | 'earningsTimestamp': 1594900801, 419 | 'earningsTimestampStart': 1602676800, 420 | 'earningsTimestampEnd': 1603123200, 421 | 'trailingPE': 82.55319, 422 | 'epsTrailingTwelveMonths': 5.922, 423 | 'epsForward': 8.78, 424 | 'sharesOutstanding': 441015008, 425 | 'bookValue': 21.166, 426 | 'fiftyDayAverage': 478.79858, 427 | 'fiftyDayAverageChange': 10.081421, 428 | 'fiftyDayAverageChangePercent': 0.021055661, 429 | 'twoHundredDayAverage': 409.49237, 430 | 'twoHundredDayAverageChange': 79.387634, 431 | 'twoHundredDayAverageChangePercent': 0.19386841, 432 | 'marketCap': 215603412992, 433 | 'forwardPE': 55.681095, 434 | 'priceToBook': 23.09742, 435 | 'sourceInterval': 15, 436 | 'exchangeDataDelayedBy': 0, 437 | 'marketState': 'POST', 438 | 'exchange': 'NMS', 439 | 'shortName': 'Netflix, Inc.', 440 | 'longName': 'Netflix, Inc.', 441 | 'messageBoardId': 'finmb_32012', 442 | 'exchangeTimezoneName': 'America/New_York', 443 | 'exchangeTimezoneShortName': 'EDT', 444 | 'gmtOffSetMilliseconds': -14400000, 445 | 'market': 'us_market', 446 | 'esgPopulated': False, 447 | 'displayName': 'Netflix', 448 | 'symbol': 'NFLX' 449 | }, { 450 | 'language': 'en-US', 451 | 'region': 'US', 452 | 'quoteType': 'EQUITY', 453 | 'quoteSourceName': 'Delayed Quote', 454 | 'triggerable': True, 455 | 'currency': 'USD', 456 | 'tradeable': False, 457 | 'firstTradeDateMilliseconds': 1092922200000, 458 | 'priceHint': 2, 459 | 'postMarketChangePercent': 0.0026999423, 460 | 'postMarketTime': 1596231815, 461 | 'postMarketPrice': 1483.0, 462 | 'postMarketChange': 0.040039062, 463 | 'regularMarketChange': -48.48999, 464 | 'regularMarketChangePercent': -3.16628, 465 | 'regularMarketTime': 1596225602, 466 | 'regularMarketPrice': 1482.96, 467 | 'regularMarketDayHigh': 1508.95, 468 | 'regularMarketDayRange': '1454.04 - 1508.95', 469 | 'regularMarketDayLow': 1454.04, 470 | 'regularMarketVolume': 3368287, 471 | 'regularMarketPreviousClose': 1531.45, 472 | 'bid': 1483.0, 473 | 'ask': 1484.0, 474 | 'bidSize': 12, 475 | 'askSize': 8, 476 | 'fullExchangeName': 'NasdaqGS', 477 | 'financialCurrency': 'USD', 478 | 'regularMarketOpen': 1505.01, 479 | 'averageDailyVolume3Month': 1643406, 480 | 'averageDailyVolume10Day': 1380212, 481 | 'fiftyTwoWeekLowChange': 469.42395, 482 | 'fiftyTwoWeekLowChangePercent': 0.46315467, 483 | 'fiftyTwoWeekRange': '1013.536 - 1586.99', 484 | 'fiftyTwoWeekHighChange': -104.03003, 485 | 'fiftyTwoWeekHighChangePercent': -0.06555179, 486 | 'fiftyTwoWeekLow': 1013.536, 487 | 'fiftyTwoWeekHigh': 1586.99, 488 | 'trailingPE': 29.915276, 489 | 'epsTrailingTwelveMonths': 49.572, 490 | 'epsForward': 55.06, 491 | 'sharesOutstanding': 336161984, 492 | 'bookValue': 297.759, 493 | 'fiftyDayAverage': 1479.2025, 494 | 'fiftyDayAverageChange': 3.7574463, 495 | 'fiftyDayAverageChangePercent': 0.0025401837, 496 | 'twoHundredDayAverage': 1375.5465, 497 | 'twoHundredDayAverageChange': 107.41345, 498 | 'twoHundredDayAverageChangePercent': 0.07808784, 499 | 'marketCap': 1014620422144, 500 | 'forwardPE': 26.933525, 501 | 'priceToBook': 4.9804034, 502 | 'sourceInterval': 15, 503 | 'exchangeDataDelayedBy': 0, 504 | 'marketState': 'POST', 505 | 'exchange': 'NMS', 506 | 'shortName': 'Alphabet Inc.', 507 | 'longName': 'Alphabet Inc.', 508 | 'messageBoardId': 'finmb_29096', 509 | 'exchangeTimezoneName': 'America/New_York', 510 | 'exchangeTimezoneShortName': 'EDT', 511 | 'gmtOffSetMilliseconds': -14400000, 512 | 'market': 'us_market', 513 | 'esgPopulated': False, 514 | 'displayName': 'Alphabet', 515 | 'symbol': 'GOOG' 516 | }] 517 | ``` 518 | 519 | ### **recommendations** 520 | 521 | === "Details" 522 | 523 | - *Description*: Get real-time quote information for given symbol(s) 524 | - *Return*: `dict` 525 | 526 | === "Example" 527 | 528 | ```python hl_lines="2" 529 | tickers = Ticker('aapl gs hasgx ^GSPC ezu') 530 | tickers.recommendations 531 | ``` 532 | 533 | === "Data" 534 | 535 | ```python 536 | { 537 | 'aapl': { 538 | 'symbol': 'AAPL', 539 | 'recommendedSymbols': [{ 540 | 'symbol': 'GOOG', 541 | 'score': 0.279041 542 | }, { 543 | 'symbol': 'AMZN', 544 | 'score': 0.278376 545 | }, { 546 | 'symbol': 'FB', 547 | 'score': 0.274481 548 | }, { 549 | 'symbol': 'TSLA', 550 | 'score': 0.225957 551 | }, { 552 | 'symbol': 'NFLX', 553 | 'score': 0.207756 554 | }] 555 | }, 556 | 'gs': { 557 | 'symbol': 'GS', 558 | 'recommendedSymbols': [{ 559 | 'symbol': 'MS', 560 | 'score': 0.195796 561 | }, { 562 | 'symbol': 'JPM', 563 | 'score': 0.160104 564 | }, { 565 | 'symbol': 'WFC', 566 | 'score': 0.139129 567 | }, { 568 | 'symbol': 'C', 569 | 'score': 0.137378 570 | }, { 571 | 'symbol': 'BAC', 572 | 'score': 0.125276 573 | }] 574 | }, 575 | 'hasgx': { 576 | 'symbol': 'HASGX', 577 | 'recommendedSymbols': [{ 578 | 'symbol': 'HAVLX', 579 | 'score': 0.020499 580 | }, { 581 | 'symbol': 'HAMGX', 582 | 'score': 0.016157 583 | }, { 584 | 'symbol': 'HASCX', 585 | 'score': 0.014594 586 | }, { 587 | 'symbol': 'HAIGX', 588 | 'score': 0.012841 589 | }, { 590 | 'symbol': 'HAMVX', 591 | 'score': 0.012294 592 | }] 593 | }, 594 | '^GSPC': { 595 | 'symbol': '^GSPC', 596 | 'recommendedSymbols': [{ 597 | 'symbol': '^TYX', 598 | 'score': 0.187618 599 | }, { 600 | 'symbol': '^IXIC', 601 | 'score': 0.157791 602 | }, { 603 | 'symbol': '^DJI', 604 | 'score': 0.134881 605 | }, { 606 | 'symbol': 'GE', 607 | 'score': 0.10353 608 | }, { 609 | 'symbol': 'MCD', 610 | 'score': 0.102003 611 | }] 612 | }, 613 | 'ezu': { 614 | 'symbol': 'EZU', 615 | 'recommendedSymbols': [{ 616 | 'symbol': 'EWQ', 617 | 'score': 0.152994 618 | }, { 619 | 'symbol': 'EWU', 620 | 'score': 0.146443 621 | }, { 622 | 'symbol': 'EWN', 623 | 'score': 0.145267 624 | }, { 625 | 'symbol': 'IEV', 626 | 'score': 0.143627 627 | }, { 628 | 'symbol': 'EWD', 629 | 'score': 0.141428 630 | }] 631 | } 632 | } 633 | ``` 634 | 635 | ### **technical_insights** 636 | 637 | === "Details" 638 | 639 | - *Description*: Technical indicators for given symbol(s) 640 | - *Return*: `dict` 641 | 642 | === "Example" 643 | 644 | ```python hl_lines="2" 645 | aapl = Ticker('aapl') 646 | aapl.technical_insights 647 | ``` 648 | 649 | === "Data" 650 | 651 | ```python 652 | { 653 | 'aapl': { 654 | 'symbol': 'aapl', 655 | 'instrumentInfo': { 656 | 'technicalEvents': { 657 | 'provider': 'Trading Central', 658 | 'sector': 'Technology', 659 | 'shortTermOutlook': { 660 | 'stateDescription': 'Recent bearish events outweigh bullish events.', 661 | 'direction': 'Bearish', 662 | 'score': 3, 663 | 'scoreDescription': 'Strong Bearish Evidence', 664 | 'sectorDirection': 'Bullish', 665 | 'sectorScore': 2, 666 | 'sectorScoreDescription': 'Bullish Evidence', 667 | 'indexDirection': 'Bearish', 668 | 'indexScore': 2, 669 | 'indexScoreDescription': 'Bearish Evidence' 670 | }, 671 | 'intermediateTermOutlook': { 672 | 'stateDescription': 'Bullish events outweigh bearish events.', 673 | 'direction': 'Bullish', 674 | 'score': 1, 675 | 'scoreDescription': 'Weak Bullish Evidence', 676 | 'sectorDirection': 'Bullish', 677 | 'sectorScore': 2, 678 | 'sectorScoreDescription': 'Bullish Evidence', 679 | 'indexDirection': 'Bullish', 680 | 'indexScore': 2, 681 | 'indexScoreDescription': 'Bullish Evidence' 682 | }, 683 | 'longTermOutlook': { 684 | 'stateDescription': 'All events are bullish.', 685 | 'direction': 'Bullish', 686 | 'score': 2, 687 | 'scoreDescription': 'Bullish Evidence', 688 | 'sectorDirection': 'Bullish', 689 | 'sectorScore': 2, 690 | 'sectorScoreDescription': 'Bullish Evidence', 691 | 'indexDirection': 'Bullish', 692 | 'indexScore': 3, 693 | 'indexScoreDescription': 'Strong Bullish Evidence' 694 | } 695 | }, 696 | 'keyTechnicals': { 697 | 'provider': 'Trading Central', 698 | 'support': 203.77, 699 | 'resistance': 388.23, 700 | 'stopLoss': 355.460616 701 | }, 702 | 'valuation': { 703 | 'color': 0.0, 704 | 'description': 'Overvalued', 705 | 'discount': '-8%', 706 | 'relativeValue': 'Premium', 707 | 'provider': 'Trading Central' 708 | } 709 | }, 710 | 'companySnapshot': { 711 | 'sectorInfo': 'Technology', 712 | 'company': { 713 | 'innovativeness': 0.9983, 714 | 'hiring': 0.9795, 715 | 'sustainability': 0.8240000000000001, 716 | 'insiderSentiments': 0.2217, 717 | 'earningsReports': 0.8340000000000001, 718 | 'dividends': 0.25 719 | }, 720 | 'sector': { 721 | 'innovativeness': 0.5, 722 | 'hiring': 0.5, 723 | 'sustainability': 0.5, 724 | 'insiderSentiments': 0.5, 725 | 'earningsReports': 0.5, 726 | 'dividends': 0.5 727 | } 728 | }, 729 | 'recommendation': { 730 | 'targetPrice': 450.0, 731 | 'provider': 'Argus Research', 732 | 'rating': 'BUY' 733 | }, 734 | 'sigDevs': [{ 735 | 'headline': 'Apple Reports Q3 Earnings Of $2.58 Per Share', 736 | 'date': '2020-07-30' 737 | }] 738 | } 739 | } 740 | ``` --------------------------------------------------------------------------------