diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 000000000..9b786ce52 Binary files /dev/null and b/.DS_Store differ diff --git a/.github/workflows/deploy_doc.yml b/.github/workflows/deploy_doc.yml new file mode 100644 index 000000000..3afaa75f2 --- /dev/null +++ b/.github/workflows/deploy_doc.yml @@ -0,0 +1,43 @@ +name: Build and Deploy Sphinx Docs + +on: + push: + branches: + - dev-documented + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Check out the repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install Sphinx==8.0.2 pydata-sphinx-theme==0.15.4 Jinja2==3.1.4 sphinx-copybutton==0.5.2 + + - name: Build Sphinx documentation + run: | + sphinx-build -b html doc/source doc/_build/html -v + + - name: List generated HTML files + run: | + ls -l -R doc/_build/html + + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_branch: documentation + publish_dir: doc/_build/html + destination_dir: docs + enable_jekyll: false diff --git a/.gitignore b/.gitignore index a2b988c12..b5d9905c4 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,10 @@ test.ipynb env/ venv/ ENV/ + +# Documentation +/doc/build/ +/doc/_build/ +/doc/source/reference/api +!yfinance.css +!/doc/source/development/assets/branches.png \ No newline at end of file diff --git a/README.md b/README.md index d631bdf53..bd47e8f39 100644 --- a/README.md +++ b/README.md @@ -32,328 +32,31 @@ Yahoo! finance API is intended for personal use only.** Star this repo Follow me on twitter +**yfinance** offers a Pythonic way to fetch financial & market data from [Yahoo!Ⓡ finance](https://finance.yahoo.com). -**yfinance** offers a threaded and Pythonic way to download market data from [Yahoo!Ⓡ finance](https://finance.yahoo.com). +## Main components -→ Check out this [Blog post](https://aroussi.com/#post/python-yahoo-finance) for a detailed tutorial with code examples. +- `Ticker`: single ticker data +- `Tickers`: multiple tickers' data +- `download`: download market data for multiple tickers +- `Sector` and `Industry`: sector and industry information +- `EquityQuery` and `Screener`: build query to screen market -[Changelog »](https://github.com/ranaroussi/yfinance/blob/main/CHANGELOG.rst) - ---- - -- [Installation](#installation) -- [Quick start](#quick-start) -- [Advanced](#logging) -- [Wiki](https://github.com/ranaroussi/yfinance/wiki) -- [Contribute](#developers-want-to-contribute) - ---- +## **NEW DOCUMENTATION WEBSITE**: [ranaroussi.github.io/yfinance](https://ranaroussi.github.io/yfinance/index.html) ## Installation -Install `yfinance` using `pip`: - -``` {.sourceCode .bash} -$ pip install yfinance --upgrade --no-cache-dir -``` - -[With Conda](https://anaconda.org/ranaroussi/yfinance). - -To install with optional dependencies, replace `optional` with: `nospam` for [caching-requests](#smarter-scraping), `repair` for [price repair](https://github.com/ranaroussi/yfinance/wiki/Price-repair), or `nospam,repair` for both: +Install `yfinance` from PYPI using `pip`: ``` {.sourceCode .bash} -$ pip install "yfinance[optional]" -``` - -[Required dependencies](./requirements.txt) , [all dependencies](./setup.py#L62). - ---- - -## Quick Start - -### The Ticker module - -The `Ticker` module, which allows you to access ticker data in a more Pythonic way: - -```python -import yfinance as yf - -msft = yf.Ticker("MSFT") - -# get all stock info -msft.info - -# get historical market data -hist = msft.history(period="1mo") - -# show meta information about the history (requires history() to be called first) -msft.history_metadata - -# show actions (dividends, splits, capital gains) -msft.actions -msft.dividends -msft.splits -msft.capital_gains # only for mutual funds & etfs - -# show share count -msft.get_shares_full(start="2022-01-01", end=None) - -# show financials: -msft.calendar -msft.sec_filings -# - income statement -msft.income_stmt -msft.quarterly_income_stmt -# - balance sheet -msft.balance_sheet -msft.quarterly_balance_sheet -# - cash flow statement -msft.cashflow -msft.quarterly_cashflow -# see `Ticker.get_income_stmt()` for more options - -# show holders -msft.major_holders -msft.institutional_holders -msft.mutualfund_holders -msft.insider_transactions -msft.insider_purchases -msft.insider_roster_holders - -msft.sustainability - -# show recommendations -msft.recommendations -msft.recommendations_summary -msft.upgrades_downgrades - -# show analysts data -msft.analyst_price_targets -msft.earnings_estimate -msft.revenue_estimate -msft.earnings_history -msft.eps_trend -msft.eps_revisions -msft.growth_estimates - -# Show future and historic earnings dates, returns at most next 4 quarters and last 8 quarters by default. -# Note: If more are needed use msft.get_earnings_dates(limit=XX) with increased limit argument. -msft.earnings_dates - -# show ISIN code - *experimental* -# ISIN = International Securities Identification Number -msft.isin - -# show options expirations -msft.options - -# show news -msft.news - -# get option chain for specific expiration -opt = msft.option_chain('YYYY-MM-DD') -# data available via: opt.calls, opt.puts -``` - -For tickers that are ETFs/Mutual Funds, `Ticker.funds_data` provides access to fund related data. - -Funds' Top Holdings and other data with category average is returned as `pd.DataFrame`. - -```python -import yfinance as yf -spy = yf.Ticker('SPY') -data = spy.funds_data - -# show fund description -data.description - -# show operational information -data.fund_overview -data.fund_operations - -# show holdings related information -data.asset_classes -data.top_holdings -data.equity_holdings -data.bond_holdings -data.bond_ratings -data.sector_weightings -``` - -If you want to use a proxy server for downloading data, use: - -```python -import yfinance as yf - -msft = yf.Ticker("MSFT") - -msft.history(..., proxy="PROXY_SERVER") -msft.get_actions(proxy="PROXY_SERVER") -msft.get_dividends(proxy="PROXY_SERVER") -msft.get_splits(proxy="PROXY_SERVER") -msft.get_capital_gains(proxy="PROXY_SERVER") -msft.get_balance_sheet(proxy="PROXY_SERVER") -msft.get_cashflow(proxy="PROXY_SERVER") -msft.option_chain(..., proxy="PROXY_SERVER") -... +$ pip install yfinance ``` -### Multiple tickers - -To initialize multiple `Ticker` objects, use - -```python -import yfinance as yf - -tickers = yf.Tickers('msft aapl goog') - -# access each ticker using (example) -tickers.tickers['MSFT'].info -tickers.tickers['AAPL'].history(period="1mo") -tickers.tickers['GOOG'].actions -``` - -To download price history into one table: - -```python -import yfinance as yf -data = yf.download("SPY AAPL", period="1mo") -``` - -#### `yf.download()` and `Ticker.history()` have many options for configuring fetching and processing. [Review the Wiki](https://github.com/ranaroussi/yfinance/wiki) for more options and detail. - -### Sector and Industry - -The `Sector` and `Industry` modules allow you to access the US market information. - -To initialize, use the relevant sector or industry key as below. (Complete mapping of the keys is available in `const.py`.) - -```python -import yfinance as yf - -tech = yf.Sector('technology') -software = yf.Industry('software-infrastructure') - -# Common information -tech.key -tech.name -tech.symbol -tech.ticker -tech.overview -tech.top_companies -tech.research_reports - -# Sector information -tech.top_etfs -tech.top_mutual_funds -tech.industries - -# Industry information -software.sector_key -software.sector_name -software.top_performing_companies -software.top_growth_companies -``` - -The modules can be chained with Ticker as below. -```python -import yfinance as yf - -# Ticker to Sector and Industry -msft = yf.Ticker('MSFT') -tech = yf.Sector(msft.info.get('sectorKey')) -software = yf.Industry(msft.info.get('industryKey')) - -# Sector and Industry to Ticker -tech_ticker = tech.ticker -tech_ticker.info -software_ticker = software.ticker -software_ticker.history() -``` - -### Market Screener -The `Screener` module allows you to screen the market based on specified queries. - -#### Query Construction -To create a query, you can use the `EquityQuery` class to construct your filters step by step. The queries support operators: `GT` (greater than), `LT` (less than), `BTWN` (between), `EQ` (equals), and logical operators `AND` and `OR` for combining multiple conditions. - -#### Screener -The `Screener` class is used to execute the queries and return the filtered results. You can set a custom body for the screener or use predefined configurations. - - - -### Logging - -`yfinance` now uses the `logging` module to handle messages, default behaviour is only print errors. If debugging, use `yf.enable_debug_mode()` to switch logging to debug with custom formatting. - -### Smarter scraping - -Install the `nospam` packages for smarter scraping using `pip` (see [Installation](#installation)). These packages help cache calls such that Yahoo is not spammed with requests. - -To use a custom `requests` session, pass a `session=` argument to -the Ticker constructor. This allows for caching calls to the API as well as a custom way to modify requests via the `User-agent` header. - -```python -import requests_cache -session = requests_cache.CachedSession('yfinance.cache') -session.headers['User-agent'] = 'my-program/1.0' -ticker = yf.Ticker('msft', session=session) -# The scraped response will be stored in the cache -ticker.actions -``` - -Combine `requests_cache` with rate-limiting to avoid triggering Yahoo's rate-limiter/blocker that can corrupt data. -```python -from requests import Session -from requests_cache import CacheMixin, SQLiteCache -from requests_ratelimiter import LimiterMixin, MemoryQueueBucket -from pyrate_limiter import Duration, RequestRate, Limiter -class CachedLimiterSession(CacheMixin, LimiterMixin, Session): - pass - -session = CachedLimiterSession( - limiter=Limiter(RequestRate(2, Duration.SECOND*5)), # max 2 requests per 5 seconds - bucket_class=MemoryQueueBucket, - backend=SQLiteCache("yfinance.cache"), -) -``` - -### Managing Multi-Level Columns - -The following answer on Stack Overflow is for [How to deal with -multi-level column names downloaded with -yfinance?](https://stackoverflow.com/questions/63107801) - -- `yfinance` returns a `pandas.DataFrame` with multi-level column - names, with a level for the ticker and a level for the stock price - data - - The answer discusses: - - How to correctly read the the multi-level columns after - saving the dataframe to a csv with `pandas.DataFrame.to_csv` - - How to download single or multiple tickers into a single - dataframe with single level column names and a ticker column - -### Persistent cache store - -To reduce Yahoo, yfinance store some data locally: timezones to localize dates, and cookie. Cache location is: - -- Windows = C:/Users/\/AppData/Local/py-yfinance -- Linux = /home/\/.cache/py-yfinance -- MacOS = /Users/\/Library/Caches/py-yfinance - -You can direct cache to use a different location with `set_tz_cache_location()`: - -```python -import yfinance as yf -yf.set_tz_cache_location("custom/cache/location") -... -``` - ---- +The list of changes can be found in the [Changelog](https://github.com/ranaroussi/yfinance/blob/main/CHANGELOG.rst) ## Developers: want to contribute? -`yfinance` relies on community to investigate bugs and contribute code. Developer guide: https://github.com/ranaroussi/yfinance/discussions/1084 +`yfinance` relies on community to investigate bugs, review code, and contribute code. Developer guide: https://github.com/ranaroussi/yfinance/discussions/1084 --- @@ -362,7 +65,6 @@ yf.set_tz_cache_location("custom/cache/location") **yfinance** is distributed under the **Apache Software License**. See the [LICENSE.txt](./LICENSE.txt) file in the release for details. - AGAIN - yfinance is **not** affiliated, endorsed, or vetted by Yahoo, Inc. It's an open-source tool that uses Yahoo's publicly available APIs, and is intended for research and educational purposes. You should refer to Yahoo!'s terms of use @@ -378,3 +80,4 @@ details on your rights to use the actual data downloaded. Please drop me a note with any feedback you have. **Ran Aroussi** + diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 000000000..d0c3cbf10 --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/doc/make.bat b/doc/make.bat new file mode 100644 index 000000000..747ffb7b3 --- /dev/null +++ b/doc/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/doc/source/_static/yfinance.css b/doc/source/_static/yfinance.css new file mode 100644 index 000000000..9fd5c1d33 --- /dev/null +++ b/doc/source/_static/yfinance.css @@ -0,0 +1,4 @@ +/* Hide the "Section Navigation" title */ +p.bd-links__title { + display: none; +} \ No newline at end of file diff --git a/doc/source/_templates/autosummary/class.rst b/doc/source/_templates/autosummary/class.rst new file mode 100644 index 000000000..12b03d75d --- /dev/null +++ b/doc/source/_templates/autosummary/class.rst @@ -0,0 +1,29 @@ +:orphan: + +{{ objname | escape | underline }} + +.. currentmodule:: {{ module }} + +.. autoclass:: {{ objname }} + + {% block attributes %} + {% if attributes %} + .. rubric:: Attributes + + {% for item in attributes %} + .. autoattribute:: {{ item }} + :noindex: + {%- endfor %} + {% endif %} + {% endblock attributes %} + + {% block methods %} + {% if methods %} + .. rubric:: Methods + + {% for item in methods %} + .. automethod:: {{ item }} + :noindex: + {%- endfor %} + {% endif %} + {% endblock methods %} diff --git a/doc/source/advanced/caching.rst b/doc/source/advanced/caching.rst new file mode 100644 index 000000000..744377241 --- /dev/null +++ b/doc/source/advanced/caching.rst @@ -0,0 +1,59 @@ +Caching +======= + +Smarter Scraping +---------------- + +Install the `nospam` package to cache API calls and reduce spam to Yahoo: + +.. code-block:: bash + + pip install yfinance[nospam] + +To use a custom `requests` session, pass a `session=` argument to +the Ticker constructor. This allows for caching calls to the API as well as a custom way to modify requests via the `User-agent` header. + +.. code-block:: python + + import requests_cache + session = requests_cache.CachedSession('yfinance.cache') + session.headers['User-agent'] = 'my-program/1.0' + ticker = yf.Ticker('MSFT', session=session) + + # The scraped response will be stored in the cache + ticker.actions + + +Combine `requests_cache` with rate-limiting to avoid triggering Yahoo's rate-limiter/blocker that can corrupt data. + +.. code-block:: python + + from requests import Session + from requests_cache import CacheMixin, SQLiteCache + from requests_ratelimiter import LimiterMixin, MemoryQueueBucket + from pyrate_limiter import Duration, RequestRate, Limiter + class CachedLimiterSession(CacheMixin, LimiterMixin, Session): + pass + + session = CachedLimiterSession( + limiter=Limiter(RequestRate(2, Duration.SECOND*5)), # max 2 requests per 5 seconds + bucket_class=MemoryQueueBucket, + backend=SQLiteCache("yfinance.cache"), + ) + + +Persistent Cache +---------------- + +To reduce Yahoo, yfinance store some data locally: timezones to localize dates, and cookie. Cache location is: + +- Windows = C:/Users/\/AppData/Local/py-yfinance +- Linux = /home/\/.cache/py-yfinance +- MacOS = /Users/\/Library/Caches/py-yfinance + +You can direct cache to use a different location with :attr:`set_tz_cache_location `: + +.. code-block:: python + + import yfinance as yf + yf.set_tz_cache_location("custom/cache/location") \ No newline at end of file diff --git a/doc/source/advanced/index.rst b/doc/source/advanced/index.rst new file mode 100644 index 000000000..e1612ccf8 --- /dev/null +++ b/doc/source/advanced/index.rst @@ -0,0 +1,11 @@ +======== +Advanced +======== + +.. toctree:: + :maxdepth: 2 + + logging + proxy + caching + multi_level_columns \ No newline at end of file diff --git a/doc/source/advanced/logging.rst b/doc/source/advanced/logging.rst new file mode 100644 index 000000000..89464871d --- /dev/null +++ b/doc/source/advanced/logging.rst @@ -0,0 +1,11 @@ +Logging +======= + +`yfinance` uses the `logging` module to handle messages. By default, only errors are logged. + +If debugging, you can switch to debug mode with custom formatting using: + +.. code-block:: python + + import yfinance as yf + yf.enable_debug_mode() diff --git a/doc/source/advanced/multi_level_columns.rst b/doc/source/advanced/multi_level_columns.rst new file mode 100644 index 000000000..434396aea --- /dev/null +++ b/doc/source/advanced/multi_level_columns.rst @@ -0,0 +1,13 @@ +************************ +Multi-Level Column Index +************************ + +The following answer on Stack Overflow is for `How to deal with +multi-level column names downloaded with yfinance? `_ + +- `yfinance` returns a `pandas.DataFrame` with multi-level column names, with a level for the ticker and a level for the stock price data + +The answer discusses: + +- How to correctly read the the multi-level columns after saving the dataframe to a csv with `pandas.DataFrame.to_csv` +- How to download single or multiple tickers into a singledataframe with single level column names and a ticker column \ No newline at end of file diff --git a/doc/source/advanced/proxy.rst b/doc/source/advanced/proxy.rst new file mode 100644 index 000000000..a1868a3bb --- /dev/null +++ b/doc/source/advanced/proxy.rst @@ -0,0 +1,11 @@ +************ +Proxy Server +************ + +You can download data via a proxy: + +.. code-block:: python + + msft = yf.Ticker("MSFT") + msft.history(..., proxy="PROXY_SERVER") + diff --git a/doc/source/conf.py b/doc/source/conf.py new file mode 100644 index 000000000..e83733b3a --- /dev/null +++ b/doc/source/conf.py @@ -0,0 +1,45 @@ +import os +import sys +sys.path.insert(0, os.path.abspath('../..')) + +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = 'yfinance - market data downloader' +copyright = '2017-2019 Ran Aroussi' +author = 'Ran Aroussi' + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = ['sphinx.ext.autodoc', + 'sphinx.ext.napoleon', + "sphinx.ext.githubpages", + "sphinx.ext.autosectionlabel", + "sphinx.ext.autosummary", + "sphinx_copybutton"] + +templates_path = ['_templates'] +exclude_patterns = [] +autoclass_content = 'both' +autosummary_generate = True +autodoc_default_options = { + 'exclude-members': '__init__' +} + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_title = 'yfinance' +html_theme = 'pydata_sphinx_theme' +html_theme_options = { + "github_url": "https://github.com/ranaroussi/yfinance", + "navbar_align": "left" +} +html_static_path = ['_static'] +html_css_files = ['yfinance.css'] diff --git a/doc/source/development/assets/branches.png b/doc/source/development/assets/branches.png new file mode 100644 index 000000000..3f620d532 Binary files /dev/null and b/doc/source/development/assets/branches.png differ diff --git a/doc/source/development/contributing.rst b/doc/source/development/contributing.rst new file mode 100644 index 000000000..0464ccfe9 --- /dev/null +++ b/doc/source/development/contributing.rst @@ -0,0 +1,109 @@ +******************************** +Contributiong to yfinance +******************************** + +`yfinance` relies on the community to investigate bugs and contribute code. Here’s how you can help: + +Contributing +------------ + +1. Fork the repository on GitHub. +2. Clone your forked repository: + + .. code-block:: bash + + git clone https://github.com/your-username/yfinance.git + +3. Create a new branch for your feature or bug fix: + + .. code-block:: bash + + git checkout -b feature-branch-name + +4. Make your changes, commit them, and push your branch to GitHub. To keep the commit history and `network graph `_ compact: + + Use short summaries for commits + + .. code-block:: shell + + git commit -m "short summary" -m "full commit message" + + **Squash** tiny or negligible commits with meaningful ones. + + .. code-block:: shell + + git rebase -i HEAD~2 + git push --force-with-lease origin + +5. Open a pull request on the `yfinance` GitHub page. + +For more information, see the `Developer Guide `_. + +Branches +--------- + +To support rapid development without breaking stable versions, this project uses a two-layer branch model: + +.. image:: assets/branches.png + :alt: Branching Model + +`Inspiration `_ + +- **dev**: New features and some bug fixes are merged here. This branch allows collective testing, conflict resolution, and further stabilization before merging into the stable branch. +- **main**: Stable branch where PIP releases are created. + +By default, branches target **main**, but most contributions should target **dev**. + +**Exceptions**: +Direct merges to **main** are allowed if: + +- `yfinance` is massively broken +- Part of `yfinance` is broken, and the fix is simple and isolated + +Unit Tests +---------- + +Tests are written using Python’s `unittest` module. Here are some ways to run tests: + +- **Run all price tests**: + + .. code-block:: shell + + python -m unittest tests.test_prices + +- **Run a subset of price tests**: + + .. code-block:: shell + + python -m unittest tests.test_prices.TestPriceRepair + +- **Run a specific test**: + + .. code-block:: shell + + python -m unittest tests.test_prices.TestPriceRepair.test_ticker_missing + +- **Run all tests**: + + .. code-block:: shell + + python -m unittest discover -s tests + +Rebasing +-------------- + +If asked to move your branch from **main** to **dev**: + +1. Ensure all relevant branches are pulled. +2. Run: + + .. code-block:: shell + + git checkout + git rebase --onto dev main + git push --force-with-lease origin + +Running the GitHub Version of yfinance +-------------------------------------- + +To download and run a GitHub version of `yfinance`, refer to `GitHub discussion `_ \ No newline at end of file diff --git a/doc/source/development/documentation.rst b/doc/source/development/documentation.rst new file mode 100644 index 000000000..7ec2c8301 --- /dev/null +++ b/doc/source/development/documentation.rst @@ -0,0 +1,46 @@ +************************************* +Contribution to the documentation +************************************* + +.. contents:: Documentation: + :local: + +About documentation +------------------------ +* yfinance documentation is written in reStructuredText (rst) and built using Sphinx. +* The documentation file is in `doc/source/..`. +* Most of the notes under API References read from class and methods docstrings. These documentations, found in `doc/source/reference/api` is autogenerated by Sphinx and not included in git. + +Building documentation locally +------------------------------- +To build the documentation locally, follow these steps: + +1. **Install Required Dependencies**: + + * Make sure `Sphinx` and any other dependencies are installed. If a `requirements.txt` file is available, you can install dependencies by running: + + .. code-block:: console + + pip install -r requirements.txt + + +2. **Build with Sphinx**: + + * After dependencies are installed, use the sphinx-build command to generate HTML documentation. + * Go to `doc/` directory Run: + + .. code-block:: console + + make clean && make html + +3. **View Documentation Locally**: + + * Open `doc/build/html/index.html` in the browser to view the generated documentation. + +Building documentation on main +------------------------------- +The documentation updates are built on merge to `main` branch. This is done via GitHub Actions workflow based on `/yfinance/.github/workflows/deploy_doc.yml`. + +1. Reivew the changes locally and push to `dev`. + +2. When `dev` gets merged to `main`, GitHub Actions workflow is automated to build documentation. \ No newline at end of file diff --git a/doc/source/development/index.rst b/doc/source/development/index.rst new file mode 100644 index 000000000..d6eb1d984 --- /dev/null +++ b/doc/source/development/index.rst @@ -0,0 +1,10 @@ +=========== +Development +=========== + +.. toctree:: + :maxdepth: 1 + + contributing + documentation + reporting_bug \ No newline at end of file diff --git a/doc/source/development/reporting_bug.rst b/doc/source/development/reporting_bug.rst new file mode 100644 index 000000000..12a6f62ea --- /dev/null +++ b/doc/source/development/reporting_bug.rst @@ -0,0 +1,5 @@ +******************************** +Reporting a Bug +******************************** + +Open a new issue on our `GitHub `_. \ No newline at end of file diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 000000000..0f7a94722 --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1,74 @@ +yfinance documentation +====================== + +Download Market Data from Yahoo! Finance's API +---------------------------------------------- + +.. admonition:: IMPORTANT LEGAL DISCLAIMER + + **Yahoo!, Y!Finance, and Yahoo! finance are registered trademarks of Yahoo, Inc.** + + yfinance is **not** affiliated, endorsed, or vetted by Yahoo, Inc. It's + an open-source tool that uses Yahoo's publicly available APIs, and is + intended for research and educational purposes. + + **You should refer to Yahoo!'s terms of use** + (`here `__), + (`here `__), + and (`here `__) + for details on your rights to use the actual data downloaded. + Remember - the Yahoo! finance API is intended for personal use only. + +Install +------- + +.. code-block:: bash + + $ pip install yfinance + +Quick start +----------- + +Showing a small sample of yfinance API, the full API is much bigger and covered in :doc:`reference/index`. + +.. code-block:: python + + import yfinance as yf + dat = yf.Ticker("MSFT") + + +One ticker symbol + +.. code-block:: python + + dat = yf.Ticker("MSFT") + dat.info + dat.calendar + dat.analyst_price_targets + dat.quarterly_income_stmt + dat.history(period='1mo') + dat.option_chain(dat.options[0]).calls + +Multiple ticker symbols + +.. code-block:: python + + tickers = yf.Tickers('MSFT AAPL GOOG') + tickers.tickers['MSFT'].info + yf.download(['MSFT', 'AAPL', 'GOOG'], period='1mo') + +Funds + +.. code-block:: python + + spy = yf.Ticker('SPY').funds_data + spy.description + spy.top_holdings + +.. toctree:: + :maxdepth: 1 + :titlesonly: + + advanced/index + reference/index + development/index diff --git a/doc/source/reference/examples/download.py b/doc/source/reference/examples/download.py new file mode 100644 index 000000000..430f20007 --- /dev/null +++ b/doc/source/reference/examples/download.py @@ -0,0 +1,2 @@ +import yfinance as yf +data = yf.download("SPY AAPL", period="1mo") \ No newline at end of file diff --git a/doc/source/reference/examples/funds_data.py b/doc/source/reference/examples/funds_data.py new file mode 100644 index 000000000..7eda35d58 --- /dev/null +++ b/doc/source/reference/examples/funds_data.py @@ -0,0 +1,18 @@ +import yfinance as yf +spy = yf.Ticker('SPY') +data = spy.funds_data + +# show fund description +data.description + +# show operational information +data.fund_overview +data.fund_operations + +# show holdings related information +data.asset_classes +data.top_holdings +data.equity_holdings +data.bond_holdings +data.bond_ratings +data.sector_weightings \ No newline at end of file diff --git a/doc/source/reference/examples/proxy.py b/doc/source/reference/examples/proxy.py new file mode 100644 index 000000000..5aaeb6132 --- /dev/null +++ b/doc/source/reference/examples/proxy.py @@ -0,0 +1,13 @@ +import yfinance as yf + +msft = yf.Ticker("MSFT") + +msft.history(..., proxy="PROXY_SERVER") +msft.get_actions(proxy="PROXY_SERVER") +msft.get_dividends(proxy="PROXY_SERVER") +msft.get_splits(proxy="PROXY_SERVER") +msft.get_capital_gains(proxy="PROXY_SERVER") +msft.get_balance_sheet(proxy="PROXY_SERVER") +msft.get_cashflow(proxy="PROXY_SERVER") +msft.option_chain(..., proxy="PROXY_SERVER") +... \ No newline at end of file diff --git a/doc/source/reference/examples/sector_industry.py b/doc/source/reference/examples/sector_industry.py new file mode 100644 index 000000000..7db3fd3a2 --- /dev/null +++ b/doc/source/reference/examples/sector_industry.py @@ -0,0 +1,25 @@ +import yfinance as yf + +tech = yf.Sector('technology') +software = yf.Industry('software-infrastructure') + +# Common information +tech.key +tech.name +tech.symbol +tech.ticker +tech.overview +tech.top_companies +tech.research_reports + +# Sector information +tech.top_etfs +tech.top_mutual_funds +tech.industries + +# Industry information +software.sector_key +software.sector_name +software.top_performing_companies +software.top_growth_companies + diff --git a/doc/source/reference/examples/sector_industry_ticker.py b/doc/source/reference/examples/sector_industry_ticker.py new file mode 100644 index 000000000..6e9c97266 --- /dev/null +++ b/doc/source/reference/examples/sector_industry_ticker.py @@ -0,0 +1,11 @@ +import yfinance as yf +# Ticker to Sector and Industry +msft = yf.Ticker('MSFT') +tech = yf.Sector(msft.info.get('sectorKey')) +software = yf.Industry(msft.info.get('industryKey')) + +# Sector and Industry to Ticker +tech_ticker = tech.ticker +tech_ticker.info +software_ticker = software.ticker +software_ticker.history() \ No newline at end of file diff --git a/doc/source/reference/examples/ticker.py b/doc/source/reference/examples/ticker.py new file mode 100644 index 000000000..00a4f7c06 --- /dev/null +++ b/doc/source/reference/examples/ticker.py @@ -0,0 +1,22 @@ +import yfinance as yf + +dat = yf.Ticker("MSFT") + +# get historical market data +dat.history(period='1mo') + +# options +dat.option_chain(dat.options[0]).calls + +# get financials +dat.balance_sheet +dat.quarterly_income_stmt + +# dates +dat.calendar + +# general info +dat.info + +# analysis +dat.analyst_price_targets diff --git a/doc/source/reference/examples/tickers.py b/doc/source/reference/examples/tickers.py new file mode 100644 index 000000000..241813019 --- /dev/null +++ b/doc/source/reference/examples/tickers.py @@ -0,0 +1,8 @@ +import yfinance as yf + +tickers = yf.Tickers('msft aapl goog') + +# access each ticker using (example) +tickers.tickers['MSFT'].info +tickers.tickers['AAPL'].history(period="1mo") +tickers.tickers['GOOG'].actions \ No newline at end of file diff --git a/doc/source/reference/index.rst b/doc/source/reference/index.rst new file mode 100644 index 000000000..f1807da27 --- /dev/null +++ b/doc/source/reference/index.rst @@ -0,0 +1,39 @@ +============= +API Reference +============= + +Overview +-------- + +The `yfinance` package provides easy access to Yahoo! Finance's API to retrieve market data. It includes classes and functions for downloading historical market data, accessing ticker information, managing cache, and more. + + +Public API +========== + +The following are the publicly available classes, and functions exposed by the `yfinance` package: + +- :attr:`Ticker `: Class for accessing single ticker data. +- :attr:`Tickers `: Class for handling multiple tickers. +- :attr:`Sector `: Domain class for accessing sector information. +- :attr:`Industry `: Domain class for accessing industry information. +- :attr:`download `: Function to download market data for multiple tickers. +- :attr:`EquityQuery `: Class to build equity market query. +- :attr:`Screener `: Class to screen the market using defined query. +- :attr:`enable_debug_mode `: Function to enable debug mode for logging. +- :attr:`set_tz_cache_location `: Function to set the timezone cache location. + +.. toctree:: + :maxdepth: 1 + :hidden: + + + yfinance.ticker_tickers + yfinance.stock + yfinance.financials + yfinance.analysis + yfinance.sector_industry + yfinance.functions + + yfinance.funds_data + yfinance.price_history diff --git a/doc/source/reference/yfinance.analysis.rst b/doc/source/reference/yfinance.analysis.rst new file mode 100644 index 000000000..27cea866c --- /dev/null +++ b/doc/source/reference/yfinance.analysis.rst @@ -0,0 +1,81 @@ +=================== +Analysis & Holdings +=================== + +.. currentmodule:: yfinance.Ticker + +Analysis +-------- + +.. autosummary:: + :toctree: api/ + :recursive: + + get_recommendations + recommendations + + get_recommendations_summary + recommendations_summary + + get_upgrades_downgrades + upgrades_downgrades + + get_sustainability + sustainability + + get_analyst_price_targets + analyst_price_targets + + get_earnings_estimate + earnings_estimate + + get_revenue_estimate + revenue_estimate + + get_earnings_history + earnings_history + + get_eps_trend + eps_trend + + get_eps_revisions + eps_revisions + + get_growth_estimates + growth_estimates + + +Holdings +-------- + +.. autosummary:: + :toctree: api/ + :recursive: + + get_funds_data + funds_data + +.. seealso:: + :meth:`yfinance.scrapers.funds.FundsData` + +.. autosummary:: + :toctree: api/ + :recursive: + + get_insider_purchases + insider_purchases + + get_insider_transactions + insider_transactions + + get_insider_roster_holders + insider_roster_holders + + get_major_holders + major_holders + + get_institutional_holders + institutional_holders + + get_mutualfund_holders + mutualfund_holders diff --git a/doc/source/reference/yfinance.financials.rst b/doc/source/reference/yfinance.financials.rst new file mode 100644 index 000000000..8a7c3ac4e --- /dev/null +++ b/doc/source/reference/yfinance.financials.rst @@ -0,0 +1,29 @@ +========== +Financials +========== + +.. currentmodule:: yfinance.Ticker + +.. autosummary:: + :toctree: api/ + :recursive: + + get_income_stmt + income_stmt + + get_balance_sheet + balance_sheet + + get_cashflow + cashflow + + get_earnings + earnings + + calendar + + get_earnings_dates + earnings_dates + + get_sec_filings + sec_filings diff --git a/doc/source/reference/yfinance.functions.rst b/doc/source/reference/yfinance.functions.rst new file mode 100644 index 000000000..ef83454b7 --- /dev/null +++ b/doc/source/reference/yfinance.functions.rst @@ -0,0 +1,51 @@ +========================= +Functions and Utilities +========================= + +.. currentmodule:: yfinance + +Download Market Data +~~~~~~~~~~~~~~~~~~~~~ +The `download` function allows you to retrieve market data for multiple tickers at once. + +.. autosummary:: + :toctree: api/ + + download + +Query Market Data +~~~~~~~~~~~~~~~~~~~~~ +The `Sector` and `Industry` modules allow you to access the sector and industry information. + +.. autosummary:: + :toctree: api/ + + EquityQuery + Screener + +.. seealso:: + :attr:`EquityQuery.valid_operand_fields ` + supported operand values for query + :attr:`EquityQuery.valid_eq_operand_map ` + supported `EQ query operand parameters` + :attr:`Screener.predefined_bodies ` + supported predefined screens + + +Enable Debug Mode +~~~~~~~~~~~~~~~~~ +Enables logging of debug information for the `yfinance` package. + +.. autosummary:: + :toctree: api/ + + enable_debug_mode + +Set Timezone Cache Location +~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Sets the cache location for timezone data. + +.. autosummary:: + :toctree: api/ + + set_tz_cache_location diff --git a/doc/source/reference/yfinance.funds_data.rst b/doc/source/reference/yfinance.funds_data.rst new file mode 100644 index 000000000..ec0d44c8f --- /dev/null +++ b/doc/source/reference/yfinance.funds_data.rst @@ -0,0 +1,11 @@ +==================== +`FundsData` class +==================== + +.. currentmodule:: yfinance.scrapers.funds + +.. autosummary:: + :toctree: api/ + :recursive: + + FundsData diff --git a/doc/source/reference/yfinance.price_history.rst b/doc/source/reference/yfinance.price_history.rst new file mode 100644 index 000000000..888870c4d --- /dev/null +++ b/doc/source/reference/yfinance.price_history.rst @@ -0,0 +1,9 @@ +==================== +`PriceHistory` class +==================== + +.. currentmodule:: yfinance.scrapers.history + +.. autoclass:: PriceHistory + :members: + :undoc-members: \ No newline at end of file diff --git a/doc/source/reference/yfinance.sector_industry.rst b/doc/source/reference/yfinance.sector_industry.rst new file mode 100644 index 000000000..14f2f1f2b --- /dev/null +++ b/doc/source/reference/yfinance.sector_industry.rst @@ -0,0 +1,32 @@ +======================= +Sector and Industry +======================= + +.. currentmodule:: yfinance + +Sector class +-------------- +The `Sector` and `Industry` modules provide access to the Sector and Industry information. + +.. autosummary:: + :toctree: api/ + :recursive: + + Sector + Industry + +.. seealso:: + :attr:`Sector.industries ` + Map of sector and industry + +Sample Code +--------------------- +To initialize, use the relevant sector or industry key as below. + +.. literalinclude:: examples/sector_industry.py + :language: python + +The modules can be chained with Ticker as below. + +.. literalinclude:: examples/sector_industry_ticker.py + :language: python diff --git a/doc/source/reference/yfinance.stock.rst b/doc/source/reference/yfinance.stock.rst new file mode 100644 index 000000000..785d5e30a --- /dev/null +++ b/doc/source/reference/yfinance.stock.rst @@ -0,0 +1,50 @@ +===== +Stock +===== + +.. currentmodule:: yfinance.Ticker + +Ticker stock methods +-------------------- + +.. autosummary:: + :toctree: api/ + :recursive: + + get_isin + isin + + history + +.. seealso:: + :meth:`yfinance.scrapers.history.PriceHistory.history` + Documentation for history + +.. autosummary:: + :toctree: api/ + :recursive: + + get_history_metadata + + get_dividends + dividends + + get_splits + splits + + get_actions + actions + + get_capital_gains + capital_gains + + get_shares_full + + get_info + info + + get_fast_info + fast_info + + get_news + news diff --git a/doc/source/reference/yfinance.ticker_tickers.rst b/doc/source/reference/yfinance.ticker_tickers.rst new file mode 100644 index 000000000..a3ff602e2 --- /dev/null +++ b/doc/source/reference/yfinance.ticker_tickers.rst @@ -0,0 +1,46 @@ +===================== +Ticker and Tickers +===================== + +.. currentmodule:: yfinance + + +Class +------------ +The `Ticker` module, allows you to access ticker data in a Pythonic way. + +.. autosummary:: + :toctree: api/ + + Ticker + Tickers + + +Ticker Sample Code +------------------ +The `Ticker` module, allows you to access ticker data in a Pythonic way. + +.. literalinclude:: examples/ticker.py + :language: python + +To initialize multiple `Ticker` objects, use + +.. literalinclude:: examples/tickers.py + :language: python + +For tickers that are ETFs/Mutual Funds, `Ticker.funds_data` provides access to fund related data. + +Funds' Top Holdings and other data with category average is returned as `pd.DataFrame`. + +.. literalinclude:: examples/funds_data.py + :language: python + +If you want to use a proxy server for downloading data, use: + +.. literalinclude:: examples/proxy.py + :language: python + +To initialize multiple `Ticker` objects, use `Tickers` module + +.. literalinclude:: examples/tickers.py + :language: python diff --git a/requirements.txt b/requirements.txt index f19ca36b1..55fea8d84 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,4 @@ pytz>=2022.5 frozendict>=2.3.4 beautifulsoup4>=4.11.1 html5lib>=1.1 -peewee>=3.16.2 +peewee>=3.16.2 \ No newline at end of file diff --git a/tests/test_ticker.py b/tests/test_ticker.py index 257e3ed06..ea95f89d8 100644 --- a/tests/test_ticker.py +++ b/tests/test_ticker.py @@ -216,11 +216,24 @@ def test_history(self): self.assertFalse(data.empty, "data is empty") def test_download(self): + tomorrow = pd.Timestamp.now().date() + pd.Timedelta(days=1) # helps with caching for t in [False, True]: for i in [False, True]: - data = yf.download(self.symbols, threads=t, ignore_tz=i) - self.assertIsInstance(data, pd.DataFrame, "data has wrong type") - self.assertFalse(data.empty, "data is empty") + for m in [False, True]: + for n in [1, 'all']: + symbols = self.symbols[0] if n == 1 else self.symbols + data = yf.download(symbols, end=tomorrow, session=self.session, + threads=t, ignore_tz=i, multi_level_index=m) + self.assertIsInstance(data, pd.DataFrame, "data has wrong type") + self.assertFalse(data.empty, "data is empty") + if i: + self.assertIsNone(data.index.tz) + else: + self.assertIsNotNone(data.index.tz) + if (not m) and n == 1: + self.assertFalse(isinstance(data.columns, pd.MultiIndex)) + else: + self.assertIsInstance(data.columns, pd.MultiIndex) def test_no_expensive_calls_introduced(self): """ @@ -825,7 +838,7 @@ def test_growth_estimates(self): columns = ['stock', 'industry', 'sector', 'index'] self.assertEqual(data.columns.values.tolist(), columns, "data has wrong column names") - index = ['0q', '+1q', '0y', '+1y', '+5y', '-5y'] + index = ['0q', '+1q', '0y', '+1y'] self.assertEqual(data.index.values.tolist(), index, "data has wrong row names") data_cached = self.ticker.growth_estimates diff --git a/yfinance/base.py b/yfinance/base.py index 96805d317..a22dd1088 100644 --- a/yfinance/base.py +++ b/yfinance/base.py @@ -560,12 +560,15 @@ def get_news(self, proxy=None) -> list: def get_earnings_dates(self, limit=12, proxy=None) -> Optional[pd.DataFrame]: """ Get earning dates (future and historic) - :param limit: max amount of upcoming and recent earnings dates to return. - Default value 12 should return next 4 quarters and last 8 quarters. - Increase if more history is needed. - - :param proxy: requests proxy to use. - :return: pandas dataframe + + Args: + limit (int): max amount of upcoming and recent earnings dates to return. + Default value 12 should return next 4 quarters and last 8 quarters. + Increase if more history is needed. + proxy: requests proxy to use. + + Returns: + pd.DataFrame """ if self._earnings_dates and limit in self._earnings_dates: return self._earnings_dates[limit] diff --git a/yfinance/const.py b/yfinance/const.py index 4e902a68f..2735f2544 100644 --- a/yfinance/const.py +++ b/yfinance/const.py @@ -427,119 +427,108 @@ } } EQUITY_SCREENER_FIELDS = { - # EQ Fields - "region", - "sector", - "peer_group", - "exchanges", - - # price - "eodprice", - "intradaypricechange", - "lastclosemarketcap.lasttwelvemonths", - "percentchange", - "lastclose52weekhigh.lasttwelvemonths", - "fiftytwowkpercentchange", - "intradayprice", - "lastclose52weeklow.lasttwelvemonths", - "intradaymarketcap", - - # trading - "beta", - "avgdailyvol3m", - "pctheldinsider", - "pctheldinst", - "dayvolume", - "eodvolume", - - # short interest - "short_percentage_of_shares_outstanding.value", - "short_interest.value", - "short_percentage_of_float.value", - "days_to_cover_short.value", - "short_interest_percentage_change.value", - - # valuation - "bookvalueshare.lasttwelvemonths", - "lastclosemarketcaptotalrevenue.lasttwelvemonths", - "lastclosetevtotalrevenue.lasttwelvemonths", - "pricebookratio.quarterly", - "peratio.lasttwelvemonths", - "lastclosepricetangiblebookvalue.lasttwelvemonths", - "lastclosepriceearnings.lasttwelvemonths", - "pegratio_5y", - - # profitability - "consecutive_years_of_dividend_growth_count", - "returnonassets.lasttwelvemonths", - "returnonequity.lasttwelvemonths", - "forward_dividend_per_share", - "forward_dividend_yield", - "returnontotalcapital.lasttwelvemonths", - - # leverage - "lastclosetevebit.lasttwelvemonths", - "netdebtebitda.lasttwelvemonths", - "totaldebtequity.lasttwelvemonths", - "ltdebtequity.lasttwelvemonths", - "ebitinterestexpense.lasttwelvemonths", - "ebitdainterestexpense.lasttwelvemonths", - "lastclosetevebitda.lasttwelvemonths", - "totaldebtebitda.lasttwelvemonths", - - # liquidity - "quickratio.lasttwelvemonths", - "altmanzscoreusingtheaveragestockinformationforaperiod.lasttwelvemonths", - "currentratio.lasttwelvemonths", - "operatingcashflowtocurrentliabilities.lasttwelvemonths", - - # income statement - "totalrevenues.lasttwelvemonths", - "netincomemargin.lasttwelvemonths", - "grossprofit.lasttwelvemonths", - "ebitda1yrgrowth.lasttwelvemonths", - "dilutedepscontinuingoperations.lasttwelvemonths", - "quarterlyrevenuegrowth.quarterly", - "epsgrowth.lasttwelvemonths", - "netincomeis.lasttwelvemonths", - "ebitda.lasttwelvemonths", - "dilutedeps1yrgrowth.lasttwelvemonths", - "totalrevenues1yrgrowth.lasttwelvemonths", - "operatingincome.lasttwelvemonths", - "netincome1yrgrowth.lasttwelvemonths", - "grossprofitmargin.lasttwelvemonths", - "ebitdamargin.lasttwelvemonths", - "ebit.lasttwelvemonths", - "basicepscontinuingoperations.lasttwelvemonths", - "netepsbasic.lasttwelvemonths" - "netepsdiluted.lasttwelvemonths", - - # balance sheet - "totalassets.lasttwelvemonths", - "totalcommonsharesoutstanding.lasttwelvemonths", - "totaldebt.lasttwelvemonths", - "totalequity.lasttwelvemonths", - "totalcurrentassets.lasttwelvemonths", - "totalcashandshortterminvestments.lasttwelvemonths", - "totalcommonequity.lasttwelvemonths", - "totalcurrentliabilities.lasttwelvemonths", - "totalsharesoutstanding", - - # cash flow - "forward_dividend_yield", - "leveredfreecashflow.lasttwelvemonths", - "capitalexpenditure.lasttwelvemonths", - "cashfromoperations.lasttwelvemonths", - "leveredfreecashflow1yrgrowth.lasttwelvemonths", - "unleveredfreecashflow.lasttwelvemonths", - "cashfromoperations1yrgrowth.lasttwelvemonths", - - # ESG - "esg_score", - "environmental_score", - "governance_score", - "social_score", - "highest_controversy" + "eq_fields": { + "region", + "sector", + "peer_group", + "exchanges"}, + "price":{ + "eodprice", + "intradaypricechange", + "lastclosemarketcap.lasttwelvemonths", + "percentchange", + "lastclose52weekhigh.lasttwelvemonths", + "fiftytwowkpercentchange", + "intradayprice", + "lastclose52weeklow.lasttwelvemonths", + "intradaymarketcap"}, + "trading":{ + "beta", + "avgdailyvol3m", + "pctheldinsider", + "pctheldinst", + "dayvolume", + "eodvolume"}, + "short_interest":{ + "short_percentage_of_shares_outstanding.value", + "short_interest.value", + "short_percentage_of_float.value", + "days_to_cover_short.value", + "short_interest_percentage_change.value"}, + "valuation":{ + "bookvalueshare.lasttwelvemonths", + "lastclosemarketcaptotalrevenue.lasttwelvemonths", + "lastclosetevtotalrevenue.lasttwelvemonths", + "pricebookratio.quarterly", + "peratio.lasttwelvemonths", + "lastclosepricetangiblebookvalue.lasttwelvemonths", + "lastclosepriceearnings.lasttwelvemonths", + "pegratio_5y"}, + "profitability":{ + "consecutive_years_of_dividend_growth_count", + "returnonassets.lasttwelvemonths", + "returnonequity.lasttwelvemonths", + "forward_dividend_per_share", + "forward_dividend_yield", + "returnontotalcapital.lasttwelvemonths"}, + "leverage":{ + "lastclosetevebit.lasttwelvemonths", + "netdebtebitda.lasttwelvemonths", + "totaldebtequity.lasttwelvemonths", + "ltdebtequity.lasttwelvemonths", + "ebitinterestexpense.lasttwelvemonths", + "ebitdainterestexpense.lasttwelvemonths", + "lastclosetevebitda.lasttwelvemonths", + "totaldebtebitda.lasttwelvemonths"}, + "liquidity":{ + "quickratio.lasttwelvemonths", + "altmanzscoreusingtheaveragestockinformationforaperiod.lasttwelvemonths", + "currentratio.lasttwelvemonths", + "operatingcashflowtocurrentliabilities.lasttwelvemonths"}, + "income_statement":{ + "totalrevenues.lasttwelvemonths", + "netincomemargin.lasttwelvemonths", + "grossprofit.lasttwelvemonths", + "ebitda1yrgrowth.lasttwelvemonths", + "dilutedepscontinuingoperations.lasttwelvemonths", + "quarterlyrevenuegrowth.quarterly", + "epsgrowth.lasttwelvemonths", + "netincomeis.lasttwelvemonths", + "ebitda.lasttwelvemonths", + "dilutedeps1yrgrowth.lasttwelvemonths", + "totalrevenues1yrgrowth.lasttwelvemonths", + "operatingincome.lasttwelvemonths", + "netincome1yrgrowth.lasttwelvemonths", + "grossprofitmargin.lasttwelvemonths", + "ebitdamargin.lasttwelvemonths", + "ebit.lasttwelvemonths", + "basicepscontinuingoperations.lasttwelvemonths", + "netepsbasic.lasttwelvemonths" + "netepsdiluted.lasttwelvemonths"}, + "balance_sheet":{ + "totalassets.lasttwelvemonths", + "totalcommonsharesoutstanding.lasttwelvemonths", + "totaldebt.lasttwelvemonths", + "totalequity.lasttwelvemonths", + "totalcurrentassets.lasttwelvemonths", + "totalcashandshortterminvestments.lasttwelvemonths", + "totalcommonequity.lasttwelvemonths", + "totalcurrentliabilities.lasttwelvemonths", + "totalsharesoutstanding"}, + "cash_flow":{ + "forward_dividend_yield", + "leveredfreecashflow.lasttwelvemonths", + "capitalexpenditure.lasttwelvemonths", + "cashfromoperations.lasttwelvemonths", + "leveredfreecashflow1yrgrowth.lasttwelvemonths", + "unleveredfreecashflow.lasttwelvemonths", + "cashfromoperations1yrgrowth.lasttwelvemonths"}, + "esg":{ + "esg_score", + "environmental_score", + "governance_score", + "social_score", + "highest_controversy"} } PREDEFINED_SCREENER_BODY_MAP = { diff --git a/yfinance/domain/domain.py b/yfinance/domain/domain.py index 5ad1ad084..5a23ea068 100644 --- a/yfinance/domain/domain.py +++ b/yfinance/domain/domain.py @@ -1,14 +1,27 @@ +from abc import ABC, abstractmethod from ..ticker import Ticker from ..const import _QUERY1_URL_ from ..data import YfData from typing import Dict, List, Optional - import pandas as _pd _QUERY_URL_ = f'{_QUERY1_URL_}/v1/finance' -class Domain: +class Domain(ABC): + """ + Abstract base class representing a domain entity in financial data, with key attributes + and methods for fetching and parsing data. Derived classes must implement the `_fetch_and_parse()` method. + """ + def __init__(self, key: str, session=None, proxy=None): + """ + Initializes the Domain object with a key, session, and proxy. + + Args: + key (str): Unique key identifying the domain entity. + session (Optional[requests.Session]): Session object for HTTP requests. Defaults to None. + proxy (Optional[Dict]): Proxy settings. Defaults to None. + """ self._key: str = key self.proxy = proxy self.session = session @@ -19,47 +32,105 @@ def __init__(self, key: str, session=None, proxy=None): self._overview: Optional[Dict] = None self._top_companies: Optional[_pd.DataFrame] = None self._research_reports: Optional[List[Dict[str, str]]] = None - + @property def key(self) -> str: + """ + Retrieves the key of the domain entity. + + Returns: + str: The unique key of the domain entity. + """ return self._key - + @property def name(self) -> str: + """ + Retrieves the name of the domain entity. + + Returns: + str: The name of the domain entity. + """ self._ensure_fetched(self._name) return self._name - + @property def symbol(self) -> str: + """ + Retrieves the symbol of the domain entity. + + Returns: + str: The symbol representing the domain entity. + """ self._ensure_fetched(self._symbol) return self._symbol - + @property def ticker(self) -> Ticker: + """ + Retrieves a Ticker object based on the domain entity's symbol. + + Returns: + Ticker: A Ticker object associated with the domain entity. + """ self._ensure_fetched(self._symbol) return Ticker(self._symbol) - + @property def overview(self) -> Dict: + """ + Retrieves the overview information of the domain entity. + + Returns: + Dict: A dictionary containing an overview of the domain entity. + """ self._ensure_fetched(self._overview) return self._overview - + @property def top_companies(self) -> Optional[_pd.DataFrame]: + """ + Retrieves the top companies within the domain entity. + + Returns: + pandas.DataFrame: A DataFrame containing the top companies in the domain. + """ self._ensure_fetched(self._top_companies) return self._top_companies - + @property def research_reports(self) -> List[Dict[str, str]]: + """ + Retrieves research reports related to the domain entity. + + Returns: + List[Dict[str, str]]: A list of research reports, where each report is a dictionary with metadata. + """ self._ensure_fetched(self._research_reports) return self._research_reports - + def _fetch(self, query_url, proxy) -> Dict: + """ + Fetches data from the given query URL. + + Args: + query_url (str): The URL used for the data query. + proxy (Dict): Proxy settings for the request. + + Returns: + Dict: The JSON response data from the request. + """ params_dict = {"formatted": "true", "withReturns": "true", "lang": "en-US", "region": "US"} result = self._data.get_raw_json(query_url, user_agent_headers=self._data.user_agent_headers, params=params_dict, proxy=proxy) return result - + def _parse_and_assign_common(self, data) -> None: + """ + Parses and assigns common data fields such as name, symbol, overview, and top companies. + + Args: + data (Dict): The raw data received from the API. + """ self._name = data.get('name') self._symbol = data.get('symbol') self._overview = self._parse_overview(data.get('overview', {})) @@ -67,6 +138,15 @@ def _parse_and_assign_common(self, data) -> None: self._research_reports = data.get('researchReports') def _parse_overview(self, overview) -> Dict: + """ + Parses the overview data for the domain entity. + + Args: + overview (Dict): The raw overview data. + + Returns: + Dict: A dictionary containing parsed overview information. + """ return { "companies_count": overview.get('companiesCount', None), "market_cap": overview.get('marketCap', {}).get('raw', None), @@ -78,6 +158,15 @@ def _parse_overview(self, overview) -> Dict: } def _parse_top_companies(self, top_companies) -> Optional[_pd.DataFrame]: + """ + Parses the top companies data and converts it into a pandas DataFrame. + + Args: + top_companies (Dict): The raw top companies data. + + Returns: + Optional[pandas.DataFrame]: A DataFrame containing top company data, or None if no data is available. + """ top_companies_column = ['symbol', 'name', 'rating', 'market weight'] top_companies_values = [(c.get('symbol'), c.get('name'), @@ -87,11 +176,22 @@ def _parse_top_companies(self, top_companies) -> Optional[_pd.DataFrame]: if not top_companies_values: return None - return _pd.DataFrame(top_companies_values, columns = top_companies_column).set_index('symbol') + return _pd.DataFrame(top_companies_values, columns=top_companies_column).set_index('symbol') + @abstractmethod def _fetch_and_parse(self) -> None: + """ + Abstract method for fetching and parsing domain-specific data. + Must be implemented by derived classes. + """ raise NotImplementedError("_fetch_and_parse() needs to be implemented by children classes") def _ensure_fetched(self, attribute) -> None: + """ + Ensures that the given attribute is fetched by calling `_fetch_and_parse()` if the attribute is None. + + Args: + attribute: The attribute to check and potentially fetch. + """ if attribute is None: self._fetch_and_parse() \ No newline at end of file diff --git a/yfinance/domain/industry.py b/yfinance/domain/industry.py index 698bce0d8..605249253 100644 --- a/yfinance/domain/industry.py +++ b/yfinance/domain/industry.py @@ -7,7 +7,17 @@ from .. import utils class Industry(Domain): + """ + Represents an industry within a sector. + """ + def __init__(self, key, session=None, proxy=None): + """ + Args: + key (str): The key identifier for the industry. + session (optional): The session to use for requests. + proxy (optional): The proxy to use for requests. + """ super(Industry, self).__init__(key, session, proxy) self._query_url = f'{_QUERY_URL_}/industries/{self._key}' @@ -17,29 +27,68 @@ def __init__(self, key, session=None, proxy=None): self._top_growth_companies = None def __repr__(self): + """ + Returns a string representation of the Industry instance. + + Returns: + str: String representation of the Industry instance. + """ return f'yfinance.Industry object <{self._key}>' @property def sector_key(self) -> str: + """ + Returns the sector key of the industry. + + Returns: + str: The sector key. + """ self._ensure_fetched(self._sector_key) return self._sector_key @property def sector_name(self) -> str: + """ + Returns the sector name of the industry. + + Returns: + str: The sector name. + """ self._ensure_fetched(self._sector_name) return self._sector_name @property def top_performing_companies(self) -> Optional[_pd.DataFrame]: + """ + Returns the top performing companies in the industry. + + Returns: + Optional[pd.DataFrame]: DataFrame containing top performing companies. + """ self._ensure_fetched(self._top_performing_companies) return self._top_performing_companies @property def top_growth_companies(self) -> Optional[_pd.DataFrame]: + """ + Returns the top growth companies in the industry. + + Returns: + Optional[pd.DataFrame]: DataFrame containing top growth companies. + """ self._ensure_fetched(self._top_growth_companies) return self._top_growth_companies def _parse_top_performing_companies(self, top_performing_companies: Dict) -> Optional[_pd.DataFrame]: + """ + Parses the top performing companies data. + + Args: + top_performing_companies (Dict): Dictionary containing top performing companies data. + + Returns: + Optional[pd.DataFrame]: DataFrame containing parsed top performing companies data. + """ compnaies_column = ['symbol','name','ytd return',' last price','target price'] compnaies_values = [(c.get('symbol', None), c.get('name', None), @@ -53,6 +102,15 @@ def _parse_top_performing_companies(self, top_performing_companies: Dict) -> Opt return _pd.DataFrame(compnaies_values, columns = compnaies_column).set_index('symbol') def _parse_top_growth_companies(self, top_growth_companies: Dict) -> Optional[_pd.DataFrame]: + """ + Parses the top growth companies data. + + Args: + top_growth_companies (Dict): Dictionary containing top growth companies data. + + Returns: + Optional[pd.DataFrame]: DataFrame containing parsed top growth companies data. + """ compnaies_column = ['symbol','name','ytd return',' growth estimate'] compnaies_values = [(c.get('symbol', None), c.get('name', None), @@ -65,6 +123,9 @@ def _parse_top_growth_companies(self, top_growth_companies: Dict) -> Optional[_p return _pd.DataFrame(compnaies_values, columns = compnaies_column).set_index('symbol') def _fetch_and_parse(self) -> None: + """ + Fetches and parses the industry data. + """ result = None try: diff --git a/yfinance/domain/sector.py b/yfinance/domain/sector.py index 2ae3a1137..c19e3523e 100644 --- a/yfinance/domain/sector.py +++ b/yfinance/domain/sector.py @@ -1,5 +1,7 @@ from __future__ import print_function from typing import Dict, Optional +from ..utils import dynamic_docstring, generate_list_table_from_dict +from ..const import SECTOR_INDUSTY_MAPPING import pandas as _pd @@ -7,48 +9,127 @@ from .. import utils class Sector(Domain): + """ + Represents a financial market sector and allows retrieval of sector-related data + such as top ETFs, top mutual funds, and industry data. + """ + def __init__(self, key, session=None, proxy=None): + """ + Args: + key (str): The key representing the sector. + session (requests.Session, optional): A session for making requests. Defaults to None. + proxy (dict, optional): A dictionary containing proxy settings for the request. Defaults to None. + + .. seealso:: + + :attr:`Sector.industries ` + Map of sector and industry + """ super(Sector, self).__init__(key, session, proxy) self._query_url: str = f'{_QUERY_URL_}/sectors/{self._key}' - self._top_etfs: Optional[Dict] = None self._top_mutual_funds: Optional[Dict] = None self._industries: Optional[_pd.DataFrame] = None def __repr__(self): + """ + Returns the string representation of the Sector object. + + Returns: + str: A string representation of the object. + """ return f'yfinance.Sector object <{self._key}>' @property def top_etfs(self) -> Dict[str, str]: + """ + Gets the top ETFs for the sector. + + Returns: + Dict[str, str]: A dictionary of ETF symbols and names. + """ self._ensure_fetched(self._top_etfs) return self._top_etfs @property def top_mutual_funds(self) -> Dict[str, str]: + """ + Gets the top mutual funds for the sector. + + Returns: + Dict[str, str]: A dictionary of mutual fund symbols and names. + """ self._ensure_fetched(self._top_mutual_funds) return self._top_mutual_funds + @dynamic_docstring({"sector_industry": generate_list_table_from_dict(SECTOR_INDUSTY_MAPPING,bullets=True)}) @property def industries(self) -> _pd.DataFrame: + """ + Gets the industries within the sector. + + Returns: + pandas.DataFrame: A DataFrame with industries' key, name, symbol, and market weight. + + {sector_industry} + """ self._ensure_fetched(self._industries) return self._industries def _parse_top_etfs(self, top_etfs: Dict) -> Dict[str, str]: + """ + Parses top ETF data from the API response. + + Args: + top_etfs (Dict): The raw ETF data from the API response. + + Returns: + Dict[str, str]: A dictionary of ETF symbols and names. + """ return {e.get('symbol'): e.get('name') for e in top_etfs} def _parse_top_mutual_funds(self, top_mutual_funds: Dict) -> Dict[str, str]: + """ + Parses top mutual funds data from the API response. + + Args: + top_mutual_funds (Dict): The raw mutual fund data from the API response. + + Returns: + Dict[str, str]: A dictionary of mutual fund symbols and names. + """ return {e.get('symbol'): e.get('name') for e in top_mutual_funds} def _parse_industries(self, industries: Dict) -> _pd.DataFrame: + """ + Parses industry data from the API response into a DataFrame. + + Args: + industries (Dict): The raw industry data from the API response. + + Returns: + pandas.DataFrame: A DataFrame containing industry key, name, symbol, and market weight. + """ industries_column = ['key','name','symbol','market weight'] industries_values = [(i.get('key'), i.get('name'), i.get('symbol'), i.get('marketWeight',{}).get('raw', None) ) for i in industries if i.get('name') != 'All Industries'] - return _pd.DataFrame(industries_values, columns = industries_column).set_index('key') + return _pd.DataFrame(industries_values, columns=industries_column).set_index('key') def _fetch_and_parse(self) -> None: + """ + Fetches and parses sector data from the API. + + Fetches data for the sector and parses the top ETFs, top mutual funds, + and industries within the sector. Stores the parsed data in the corresponding + attributes `_top_etfs`, `_top_mutual_funds`, and `_industries`. + + Raises: + Exception: If fetching or parsing the sector data fails. + """ result = None try: diff --git a/yfinance/multi.py b/yfinance/multi.py index ba0646b6d..7da0b3ff8 100644 --- a/yfinance/multi.py +++ b/yfinance/multi.py @@ -24,6 +24,7 @@ import logging import time as _time import traceback +from typing import Union import multitasking as _multitasking import pandas as _pd @@ -38,8 +39,9 @@ def download(tickers, start=None, end=None, actions=False, threads=True, ignore_tz=None, group_by='column', auto_adjust=False, back_adjust=False, repair=False, keepna=False, progress=True, period="max", interval="1d", prepost=False, proxy=None, rounding=False, timeout=10, session=None, - multi_level_index=True): - """Download yahoo tickers + multi_level_index=True) -> Union[_pd.DataFrame, None]: + """ + Download yahoo tickers :Parameters: tickers : str, list List of tickers to download @@ -210,7 +212,7 @@ def download(tickers, start=None, end=None, actions=False, threads=True, _realign_dfs() data = _pd.concat(shared._DFS.values(), axis=1, sort=True, keys=shared._DFS.keys(), names=['Ticker', 'Price']) - data.index = _pd.to_datetime(data.index, utc=True) + data.index = _pd.to_datetime(data.index, utc=not ignore_tz) # switch names back to isins if applicable data.rename(columns=shared._ISINS, inplace=True) diff --git a/yfinance/scrapers/analysis.py b/yfinance/scrapers/analysis.py index f2778f283..8bdc94290 100644 --- a/yfinance/scrapers/analysis.py +++ b/yfinance/scrapers/analysis.py @@ -205,13 +205,18 @@ def growth_estimates(self) -> pd.DataFrame: self._growth_estimates = pd.DataFrame() return self._growth_estimates + # LTG is not defined in yahoo finance front-end as at 2024-11-14. + # But its addition is breaking the retrieval of growth estimates. + # Also, support for 5 year seem to have dropped. + # TODO: Revisit this change and consider permanently removing these keys. data_dict = { '0q': [], '+1q': [], '0y': [], '+1y': [], - '+5y': [], - '-5y': [] + # 'LTG': [], + # '+5y': [], + # '-5y': [] } # make sure no column is empty @@ -222,19 +227,23 @@ def growth_estimates(self) -> pd.DataFrame: for item in self._earnings_trend: period = item['period'] - data_dict[period].append(item.get('growth', {}).get('raw', None)) + if period in data_dict: + data_dict[period].append(item.get('growth', {}).get('raw', None)) for item in industry_trend: period = item['period'] - data_dict[period].append(item.get('growth', None)) + if period in data_dict: + data_dict[period].append(item.get('growth', None)) for item in sector_trend: period = item['period'] - data_dict[period].append(item.get('growth', None)) + if period in data_dict: + data_dict[period].append(item.get('growth', None)) for item in index_trend: period = item['period'] - data_dict[period].append(item.get('growth', None)) + if period in data_dict: + data_dict[period].append(item.get('growth', None)) cols = ['stock', 'industry', 'sector', 'index'] self._growth_estimates = pd.DataFrame(data_dict, index=cols).T diff --git a/yfinance/scrapers/funds.py b/yfinance/scrapers/funds.py index 61faef615..ef760d9a5 100644 --- a/yfinance/scrapers/funds.py +++ b/yfinance/scrapers/funds.py @@ -9,15 +9,21 @@ _QUOTE_SUMMARY_URL_ = f"{_BASE_URL_}/v10/finance/quoteSummary/" -''' -Supports ETF and Mutual Funds Data -Queried Modules: quoteType, summaryProfile, fundProfile, topHoldings - -Notes: -- fundPerformance module is not implemented as better data is queryable using history -''' class FundsData: + """ + ETF and Mutual Funds Data + Queried Modules: quoteType, summaryProfile, fundProfile, topHoldings + + Notes: + - fundPerformance module is not implemented as better data is queryable using history + """ def __init__(self, data: YfData, symbol: str, proxy=None): + """ + Args: + data (YfData): The YfData object for fetching data. + symbol (str): The symbol of the fund. + proxy (optional): Proxy settings for fetching data. + """ self._data = data self._symbol = symbol self.proxy = proxy @@ -41,71 +47,143 @@ def __init__(self, data: YfData, symbol: str, proxy=None): self._sector_weightings = None def quote_type(self) -> str: + """ + Returns the quote type of the fund. + + Returns: + str: The quote type. + """ if self._quote_type is None: self._fetch_and_parse() return self._quote_type @property def description(self) -> str: + """ + Returns the description of the fund. + + Returns: + str: The description. + """ if self._description is None: self._fetch_and_parse() return self._description @property def fund_overview(self) -> Dict[str, Optional[str]]: + """ + Returns the fund overview. + + Returns: + Dict[str, Optional[str]]: The fund overview. + """ if self._fund_overview is None: self._fetch_and_parse() return self._fund_overview @property def fund_operations(self) -> pd.DataFrame: + """ + Returns the fund operations. + + Returns: + pd.DataFrame: The fund operations. + """ if self._fund_operations is None: self._fetch_and_parse() return self._fund_operations @property def asset_classes(self) -> Dict[str, float]: + """ + Returns the asset classes of the fund. + + Returns: + Dict[str, float]: The asset classes. + """ if self._asset_classes is None: self._fetch_and_parse() return self._asset_classes @property def top_holdings(self) -> pd.DataFrame: + """ + Returns the top holdings of the fund. + + Returns: + pd.DataFrame: The top holdings. + """ if self._top_holdings is None: self._fetch_and_parse() return self._top_holdings @property def equity_holdings(self) -> pd.DataFrame: + """ + Returns the equity holdings of the fund. + + Returns: + pd.DataFrame: The equity holdings. + """ if self._equity_holdings is None: self._fetch_and_parse() return self._equity_holdings @property def bond_holdings(self) -> pd.DataFrame: + """ + Returns the bond holdings of the fund. + + Returns: + pd.DataFrame: The bond holdings. + """ if self._bond_holdings is None: self._fetch_and_parse() return self._bond_holdings @property def bond_ratings(self) -> Dict[str, float]: + """ + Returns the bond ratings of the fund. + + Returns: + Dict[str, float]: The bond ratings. + """ if self._bond_ratings is None: self._fetch_and_parse() return self._bond_ratings @property def sector_weightings(self) -> Dict[str,float]: + """ + Returns the sector weightings of the fund. + + Returns: + Dict[str, float]: The sector weightings. + """ if self._sector_weightings is None: self._fetch_and_parse() return self._sector_weightings def _fetch(self, proxy): + """ + Fetches the raw JSON data from the API. + + Args: + proxy: Proxy settings for fetching data. + + Returns: + dict: The raw JSON data. + """ modules = ','.join(["quoteType", "summaryProfile", "topHoldings", "fundProfile"]) params_dict = {"modules": modules, "corsDomain": "finance.yahoo.com", "symbol": self._symbol, "formatted": "false"} result = self._data.get_raw_json(_QUOTE_SUMMARY_URL_+self._symbol, user_agent_headers=self._data.user_agent_headers, params=params_dict, proxy=proxy) return result def _fetch_and_parse(self) -> None: + """ + Fetches and parses the data from the API. + """ result = self._fetch(self.proxy) try: data = result["quoteSummary"]["result"][0] @@ -128,15 +206,37 @@ def _fetch_and_parse(self) -> None: @staticmethod def _parse_raw_values(data, default=None): + """ + Parses raw values from the data. + + Args: + data: The data to parse. + default: The default value if data is not a dictionary. + + Returns: + The parsed value or the default value. + """ if not isinstance(data, dict): return data return data.get("raw", default) def _parse_description(self, data) -> None: + """ + Parses the description from the data. + + Args: + data: The data to parse. + """ self._description = data.get("longBusinessSummary", "") - def _parse_top_holdings(self, data) -> None: # done + def _parse_top_holdings(self, data) -> None: + """ + Parses the top holdings from the data. + + Args: + data: The data to parse. + """ # asset classes self._asset_classes = { "cashPosition": self._parse_raw_values(data.get("cashPosition", None)), @@ -207,6 +307,12 @@ def _parse_top_holdings(self, data) -> None: # done self._sector_weightings = dict((key, d[key]) for d in data.get("sectorWeightings", []) for key in d) def _parse_fund_profile(self, data): + """ + Parses the fund profile from the data. + + Args: + data: The data to parse. + """ self._fund_overview = { "categoryName": data.get("categoryName", None), "family": data.get("family", None), diff --git a/yfinance/scrapers/history.py b/yfinance/scrapers/history.py index 20958e2d8..bda169eaa 100644 --- a/yfinance/scrapers/history.py +++ b/yfinance/scrapers/history.py @@ -953,7 +953,7 @@ def _standardise_currency(self, df, currency): if prices_in_subunits: for c in _PRICE_COLNAMES_: df[c] *= m - self._history_metadata["currency"] = currency + self._history_metadata["currency"] = currency2 f_div = df['Dividends']!=0.0 if f_div.any(): diff --git a/yfinance/screener/screener.py b/yfinance/screener/screener.py index 2d35968e8..cf6e16881 100644 --- a/yfinance/screener/screener.py +++ b/yfinance/screener/screener.py @@ -4,11 +4,28 @@ from yfinance.data import YfData from yfinance.const import _BASE_URL_, PREDEFINED_SCREENER_BODY_MAP from .screener_query import Query +from ..utils import dynamic_docstring, generate_list_table_from_dict_of_dict _SCREENER_URL_ = f"{_BASE_URL_}/v1/finance/screener" class Screener: + """ + The `Screener` class is used to execute the queries and return the filtered results. + + The Screener class provides methods to set and manipulate the body of a screener request, + fetch and parse the screener results, and access predefined screener bodies. + """ def __init__(self, session=None, proxy=None): + """ + Args: + session (requests.Session, optional): A requests session object to be used for making HTTP requests. Defaults to None. + proxy (str, optional): A proxy URL to be used for making HTTP requests. Defaults to None. + + .. seealso:: + + :attr:`Screener.predefined_bodies ` + supported predefined screens + """ self.proxy = proxy self.session = session @@ -25,17 +42,41 @@ def body(self) -> Dict: @property def response(self) -> Dict: + """ + Fetch screen result + + Example: + + .. code-block:: python + + result = screener.response + symbols = [quote['symbol'] for quote in result['quotes']] + """ if self._body_updated or self._response is None: self._fetch_and_parse() self._body_updated = False return self._response + @dynamic_docstring({"predefined_screeners": generate_list_table_from_dict_of_dict(PREDEFINED_SCREENER_BODY_MAP,bullets=False)}) @property def predefined_bodies(self) -> Dict: + """ + Predefined Screeners + {predefined_screeners} + """ return self._predefined_bodies def set_default_body(self, query: Query, offset: int = 0, size: int = 100, sortField: str = "ticker", sortType: str = "desc", quoteType: str = "equity", userId: str = "", userIdType: str = "guid") -> None: + """ + Set the default body using a custom query + + Example: + + .. code-block:: python + + screener.set_default_body(qf) + """ self._body_updated = True self._body = { @@ -50,6 +91,21 @@ def set_default_body(self, query: Query, offset: int = 0, size: int = 100, sortF } def set_predefined_body(self, k: str) -> None: + """ + Set a predefined body + + Example: + + .. code-block:: python + + screener.set_predefined_body('day_gainers') + + + .. seealso:: + + :attr:`Screener.predefined_bodies ` + supported predefined screens + """ body = PREDEFINED_SCREENER_BODY_MAP.get(k, None) if not body: raise ValueError(f'Invalid key {k} provided for predefined screener') @@ -58,6 +114,24 @@ def set_predefined_body(self, k: str) -> None: self._body = body def set_body(self, body: Dict) -> None: + """ + Set the fully custom body + + Example: + + .. code-block:: python + + screener.set_body({ + "offset": 0, + "size": 100, + "sortField": "ticker", + "sortType": "desc", + "quoteType": "equity", + "query": qf.to_dict(), + "userId": "", + "userIdType": "guid" + }) + """ missing_keys = [key for key in self._accepted_body_keys if key not in body] if missing_keys: raise ValueError(f"Missing required keys in body: {missing_keys}") @@ -71,6 +145,15 @@ def set_body(self, body: Dict) -> None: def patch_body(self, values: Dict) -> None: + """ + Patch parts of the body + + Example: + + .. code-block:: python + + screener.patch_body({"offset": 100}) + """ extra_keys = [key for key in values if key not in self._accepted_body_keys] if extra_keys: raise ValueError(f"Body contains extra keys: {extra_keys}") diff --git a/yfinance/screener/screener_query.py b/yfinance/screener/screener_query.py index a027af509..65c937591 100644 --- a/yfinance/screener/screener_query.py +++ b/yfinance/screener/screener_query.py @@ -1,19 +1,67 @@ +from abc import ABC, abstractmethod import numbers -from typing import List, Union, Dict, Set +from typing import List, Union, Dict from yfinance.const import EQUITY_SCREENER_EQ_MAP, EQUITY_SCREENER_FIELDS from yfinance.exceptions import YFNotImplementedError +from ..utils import dynamic_docstring, generate_list_table_from_dict -class Query: +class Query(ABC): def __init__(self, operator: str, operand: Union[numbers.Real, str, List['Query']]): self.operator = operator self.operands = operand - + + @abstractmethod def to_dict(self) -> Dict: raise YFNotImplementedError('to_dict() needs to be implemented by children classes') class EquityQuery(Query): + """ + The `EquityQuery` class constructs filters for stocks based on specific criteria such as region, sector, exchange, and peer group. + + The queries support operators: `GT` (greater than), `LT` (less than), `BTWN` (between), `EQ` (equals), and logical operators `AND` and `OR` for combining multiple conditions. + + Example: + Screen for stocks where the end-of-day price is greater than 3. + + .. code-block:: python + + gt = yf.EquityQuery('gt', ['eodprice', 3]) + + Screen for stocks where the average daily volume over the last 3 months is less than a very large number. + + .. code-block:: python + + lt = yf.EquityQuery('lt', ['avgdailyvol3m', 99999999999]) + + Screen for stocks where the intraday market cap is between 0 and 100 million. + + .. code-block:: python + + btwn = yf.EquityQuery('btwn', ['intradaymarketcap', 0, 100000000]) + + Screen for stocks in the Technology sector. + + .. code-block:: python + + eq = yf.EquityQuery('eq', ['sector', 'Technology']) + + Combine queries using AND/OR. + + .. code-block:: python + + qt = yf.EquityQuery('and', [gt, lt]) + qf = yf.EquityQuery('or', [qt, btwn, eq]) + """ def __init__(self, operator: str, operand: Union[numbers.Real, str, List['EquityQuery']]): + """ + .. seealso:: + + :attr:`EquityQuery.valid_operand_fields ` + supported operand values for query + :attr:`EquityQuery.valid_eq_operand_map ` + supported `EQ query operand parameters` + """ operator = operator.upper() if not isinstance(operand, list): @@ -34,16 +82,26 @@ def __init__(self, operator: str, operand: Union[numbers.Real, str, List['Equity self.operator = operator self.operands = operand - self._valid_eq_map = EQUITY_SCREENER_EQ_MAP - self._valid_fields = EQUITY_SCREENER_FIELDS - + self._valid_eq_operand_map = EQUITY_SCREENER_EQ_MAP + self._valid_operand_fields = EQUITY_SCREENER_FIELDS + + @dynamic_docstring({"valid_eq_operand_map_table": generate_list_table_from_dict(EQUITY_SCREENER_EQ_MAP)}) @property - def valid_eq_map(self) -> Dict: - return self._valid_eq_map + def valid_eq_operand_map(self) -> Dict: + """ + Valid Operand Map for Operator "EQ" + {valid_eq_operand_map_table} + """ + return self._valid_eq_operand_map + @dynamic_docstring({"valid_operand_fields_table": generate_list_table_from_dict(EQUITY_SCREENER_FIELDS)}) @property - def valid_fields(self) -> Set: - return self._valid_fields + def valid_operand_fields(self) -> Dict: + """ + Valid Operand Fields + {valid_operand_fields_table} + """ + return self._valid_operand_fields def _validate_or_and_operand(self, operand: List['EquityQuery']) -> None: if len(operand) <= 1: @@ -54,7 +112,8 @@ def _validate_or_and_operand(self, operand: List['EquityQuery']) -> None: def _validate_eq_operand(self, operand: List[Union[str, numbers.Real]]) -> None: if len(operand) != 2: raise ValueError('Operand must be length 2 for EQ') - if operand[0] not in EQUITY_SCREENER_FIELDS: + + if not any(operand[0] in fields_by_type for fields_by_type in EQUITY_SCREENER_FIELDS.values()): raise ValueError('Invalid field for Screener') if operand[0] not in EQUITY_SCREENER_EQ_MAP: raise ValueError('Invalid EQ key') @@ -64,7 +123,7 @@ def _validate_eq_operand(self, operand: List[Union[str, numbers.Real]]) -> None: def _validate_btwn_operand(self, operand: List[Union[str, numbers.Real]]) -> None: if len(operand) != 3: raise ValueError('Operand must be length 3 for BTWN') - if operand[0] not in EQUITY_SCREENER_FIELDS: + if not any(operand[0] in fields_by_type for fields_by_type in EQUITY_SCREENER_FIELDS.values()): raise ValueError('Invalid field for Screener') if isinstance(operand[1], numbers.Real) is False: raise TypeError('Invalid comparison type for BTWN') @@ -74,7 +133,7 @@ def _validate_btwn_operand(self, operand: List[Union[str, numbers.Real]]) -> Non def _validate_gt_lt(self, operand: List[Union[str, numbers.Real]]) -> None: if len(operand) != 2: raise ValueError('Operand must be length 2 for GT/LT') - if operand[0] not in EQUITY_SCREENER_FIELDS: + if not any(operand[0] in fields_by_type for fields_by_type in EQUITY_SCREENER_FIELDS.values()): raise ValueError('Invalid field for Screener') if isinstance(operand[1], numbers.Real) is False: raise TypeError('Invalid comparison type for GT/LT') diff --git a/yfinance/utils.py b/yfinance/utils.py index ebc8b99af..612e70b9c 100644 --- a/yfinance/utils.py +++ b/yfinance/utils.py @@ -199,7 +199,7 @@ def get_all_by_isin(isin, proxy=None, session=None): 'ticker': { 'symbol': ticker['symbol'], 'shortname': ticker['shortname'], - 'longname': ticker['longname'], + 'longname': ticker.get('longname',''), 'type': ticker['quoteType'], 'exchange': ticker['exchDisp'], }, @@ -932,3 +932,64 @@ def __update_amount(self, new_amount): def __str__(self): return str(self.prog_bar) +def dynamic_docstring(placeholders: dict): + """ + A decorator to dynamically update the docstring of a function or method. + + Args: + placeholders (dict): A dictionary where keys are placeholder names and values are the strings to insert. + """ + def decorator(func): + if func.__doc__: + docstring = func.__doc__ + # Replace each placeholder with its corresponding value + for key, value in placeholders.items(): + docstring = docstring.replace(f"{{{key}}}", value) + func.__doc__ = docstring + return func + return decorator + +def _generate_table_configurations() -> str: + import textwrap + table = textwrap.dedent(""" + .. list-table:: Permitted Keys/Values + :widths: 25 75 + :header-rows: 1 + + * - Key + - Values + """) + + return table + +def generate_list_table_from_dict(data: dict, bullets: bool=True) -> str: + """ + Generate a list-table for the docstring showing permitted keys/values. + """ + table = _generate_table_configurations() + for key, values in data.items(): + value_str = ', '.join(sorted(values)) + table += f" * - {key}\n" + if bullets: + table += " -\n" + for value in sorted(values): + table += f" - {value}\n" + else: + table += f" - {value_str}\n" + return table + +def generate_list_table_from_dict_of_dict(data: dict, bullets: bool=True) -> str: + """ + Generate a list-table for the docstring showing permitted keys/values. + """ + table = _generate_table_configurations() + for key, values in data.items(): + value_str = values + table += f" * - {key}\n" + if bullets: + table += " -\n" + for value in sorted(values): + table += f" - {value}\n" + else: + table += f" - {value_str}\n" + return table \ No newline at end of file