From deafc88520edbab708dfd44989bf7309995e799e Mon Sep 17 00:00:00 2001 From: Eric Pien Date: Sun, 3 Nov 2024 05:34:13 -0800 Subject: [PATCH 1/4] Add Sphinx Documentation - Initial draft of Sphinx documentation using rst - update docstrings - add deploy_doc.yml for automated deployment through GitHub Actions --- .DS_Store | Bin 0 -> 6148 bytes .github/workflows/deploy_doc.yml | 42 +++ .gitignore | 7 + README.md | 305 +----------------- doc/Makefile | 20 ++ doc/make.bat | 35 ++ doc/source/_static/yfinance.css | 4 + doc/source/_templates/autosummary/class.rst | 30 ++ doc/source/conf.py | 45 +++ doc/source/development/assets/branches.png | Bin 0 -> 41810 bytes doc/source/development/contributing.rst | 109 +++++++ doc/source/development/documentation.rst | 46 +++ doc/source/development/index.rst | 9 + doc/source/development/reporting_bug.rst | 5 + doc/source/getting_started/index.rst | 9 + doc/source/getting_started/installation.rst | 17 + doc/source/getting_started/legal.rst | 12 + doc/source/getting_started/quick_start.rst | 30 ++ doc/source/index.rst | 30 ++ doc/source/reference/examples/download.py | 2 + doc/source/reference/examples/funds_data.py | 18 ++ doc/source/reference/examples/proxy.py | 13 + .../reference/examples/sector_industry.py | 25 ++ .../examples/sector_industry_ticker.py | 11 + doc/source/reference/examples/ticker.py | 77 +++++ doc/source/reference/examples/tickers.py | 8 + doc/source/reference/index.rst | 33 ++ doc/source/reference/yfinance.functions.rst | 51 +++ .../reference/yfinance.sector_industry.rst | 32 ++ .../reference/yfinance.ticker_tickers.rst | 46 +++ doc/source/user_guide/index.rst | 11 + doc/source/user_guide/logging.rst | 11 + doc/source/user_guide/multi_level_columns.rst | 13 + doc/source/user_guide/persistent_cache.rst | 16 + doc/source/user_guide/proxy.rst | 11 + doc/source/user_guide/smart_scraping.rst | 41 +++ requirements.txt | 2 +- yfinance/base.py | 15 +- yfinance/const.py | 215 ++++++------ yfinance/domain/domain.py | 124 ++++++- yfinance/domain/industry.py | 61 ++++ yfinance/domain/sector.py | 85 ++++- yfinance/multi.py | 86 +++-- yfinance/scrapers/funds.py | 122 ++++++- yfinance/screener/screener.py | 83 +++++ yfinance/screener/screener_query.py | 85 ++++- yfinance/utils.py | 61 ++++ 47 files changed, 1613 insertions(+), 500 deletions(-) create mode 100644 .DS_Store create mode 100644 .github/workflows/deploy_doc.yml create mode 100644 doc/Makefile create mode 100644 doc/make.bat create mode 100644 doc/source/_static/yfinance.css create mode 100644 doc/source/_templates/autosummary/class.rst create mode 100644 doc/source/conf.py create mode 100644 doc/source/development/assets/branches.png create mode 100644 doc/source/development/contributing.rst create mode 100644 doc/source/development/documentation.rst create mode 100644 doc/source/development/index.rst create mode 100644 doc/source/development/reporting_bug.rst create mode 100644 doc/source/getting_started/index.rst create mode 100644 doc/source/getting_started/installation.rst create mode 100644 doc/source/getting_started/legal.rst create mode 100644 doc/source/getting_started/quick_start.rst create mode 100644 doc/source/index.rst create mode 100644 doc/source/reference/examples/download.py create mode 100644 doc/source/reference/examples/funds_data.py create mode 100644 doc/source/reference/examples/proxy.py create mode 100644 doc/source/reference/examples/sector_industry.py create mode 100644 doc/source/reference/examples/sector_industry_ticker.py create mode 100644 doc/source/reference/examples/ticker.py create mode 100644 doc/source/reference/examples/tickers.py create mode 100644 doc/source/reference/index.rst create mode 100644 doc/source/reference/yfinance.functions.rst create mode 100644 doc/source/reference/yfinance.sector_industry.rst create mode 100644 doc/source/reference/yfinance.ticker_tickers.rst create mode 100644 doc/source/user_guide/index.rst create mode 100644 doc/source/user_guide/logging.rst create mode 100644 doc/source/user_guide/multi_level_columns.rst create mode 100644 doc/source/user_guide/persistent_cache.rst create mode 100644 doc/source/user_guide/proxy.rst create mode 100644 doc/source/user_guide/smart_scraping.rst diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..9b786ce52aa00e90336160999621dda530c39633 GIT binary patch literal 6148 zcmeH~J!%6%427R!7lt%jrkutH$PET#pTHLg*pSB9U`XmYdY*ooY+RcqJc0B^niaeI z6+0^cw);B20~3G^-4$C8Gc)EZoN>eH`*^=zZr4v%yb8QT%#4)@v;EqZh=2%)fCz|y z2rP&|p5oZNF6f!`C?X&N%OK$2hemhpr6V;y9Sku7P}eMnaUHV+wRwTsOGhdzG^^>s zs?}l)@p`mVTV2;nM{2giYWT3av-uQ5vuuYoCN%3I3L+o^GXks3CqMrW^hfjmtVO8^ zh`>K1VC(&HzvD~Q+4|@8y#7&DUk^GpE@$}l6Trle;x~F2_nR-My>z6qLeq~xU{DZ& HrxN%9njH~; literal 0 HcmV?d00001 diff --git a/.github/workflows/deploy_doc.yml b/.github/workflows/deploy_doc.yml new file mode 100644 index 000000000..400a91492 --- /dev/null +++ b/.github/workflows/deploy_doc.yml @@ -0,0 +1,42 @@ +name: Build and Deploy Sphinx Docs + +on: + push: + branches: + - dev-documented + +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 \ No newline at end of file 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..2c22fdd9a 100644 --- a/README.md +++ b/README.md @@ -35,19 +35,16 @@ Yahoo! finance API is intended for personal use only.** **yfinance** offers a threaded and Pythonic way to download market data from [Yahoo!Ⓡ finance](https://finance.yahoo.com). -→ Check out this [Blog post](https://aroussi.com/#post/python-yahoo-finance) for a detailed tutorial with code examples. +## Main Features +- `Ticker` module: Class for accessing single ticker data. +- `Tickers` module: Class for handling multiple tickers. +- `download` Efficiently download market data for multiple tickers. +- `Sector` and `Industry` modules : Classes for accessing sector and industry information. +- Market Screening: `EquityQuery` and `Screener` to build query and screen the market. +- Caching and Smart Scraping -[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) - ---- +## Documentation +The official documentation is available on [ranaroussi.github.io/yfinance](https://ranaroussi.github.io/yfinance/index.html) ## Installation @@ -67,292 +64,10 @@ $ 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") -... -``` - -### 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 +The list of changes can be found in the [changelog](https://github.com/ranaroussi/yfinance/blob/main/CHANGELOG.rst) -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") -... -``` - ---- ## Developers: want to contribute? - `yfinance` relies on community to investigate bugs and contribute code. Developer guide: https://github.com/ranaroussi/yfinance/discussions/1084 --- 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..a2fbdb96f --- /dev/null +++ b/doc/source/_templates/autosummary/class.rst @@ -0,0 +1,30 @@ +{{ fullname | escape | underline}} + +.. currentmodule:: {{ module }} + +.. autoclass:: {{ objname }} + + {% block attributes %} + {% if attributes %} + .. rubric:: Attributes + + .. autosummary:: + :toctree: attributes + {% for item in attributes %} + ~{{ name }}.{{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + + {% block methods %} + {% if methods %} + .. rubric:: Methods + + .. autosummary:: + :toctree: methods + {% for item in methods %} + ~{{ name }}.{{ item }} + {%- endfor %} + {% endif %} + {% endblock %} \ No newline at end of file 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 0000000000000000000000000000000000000000..3f620d532279b6209cbfa2cddf03164a3e66c282 GIT binary patch literal 41810 zcmY(qbyOTpw>=yP39dp*aK88mo;hQZxoU~qSWyAAHYdA@s} z_r2fz(`)tW)iqUhYM-F}(BqsF5Z29gmUj;5q*Axx zU|Ms(Os%9;$29AmOK1SqcN9Xze=nb~kaTv=_#7lUaoCYC5dL$a`5rvM^yB~kr~y(m z`%Vah88+}y)pTb#8P#rs_`ffj#AWYJzrMXEcM6OSdkS_i+SQp+BKN z8r}FJd84b2@84Mk9#DO!@wocKraz=s9R9dTLA?gdrBN=qa$)MjXOX1$e@qwlpCf~T zxD4$kjGOtNH8|4(0fHJCRtxevgDSAgk%6HT5(iq6mu>c@>zG`9K^9@{VSZ!nZc_#N z4k^?BuIf1op;7dJ)Yegrl%NMb2THGshcym!x|G*C^QsHsHn#i zIQx{a1{~`bv5bcp_(a5?AX9`NbpWs1R@a)6aHlGnajW)0Z!WtAK^{SyQjE8PP2QA-#$lMAbPc8rKElT5y6($a(}La$8bh2 zlV)A}oR9Aql#>V5ilG=3pQX(e9oD}-7@Qmnd>ZxrD+KZYZ5KUhdJHA_tkCBAL=;1( ziCAsPOtMlyj8nymA6mZzj;4$Du^6>@YWL@GOd(%DdL)Hsh-0R_OzjaUNT+Iu2F^}-n4&J`ya zr)asYK?W&*cO3!KfG?Fbv5<~uI`FIxwa_swY(j>5&5vQSQeUHz#2!cB=uf#YgeNS> zb|hKDP6ygJfR#|=;IHXU_!w(T9y&PeSXfb4i&cz6B-5)?8(b;SbVy1(h(9V?Ep_;d zCrF_u0isc{)Ew_23yr0KGQ4ArPdQvAxp}^sbQ&otd}bl=>hD`ZR6Nk*ZM)BXh7J?e z=w+Qrm9%A%)w1FQgHxj37$4@0-IYFZ{^xiG;!%A!u@$8x(E5H-LMAl2-_VOkZTaYE zNjSl4;8zFH_b`iCzZkQqi<6HY=M=rkg0w$Gme*%MIs>U?NE7IydaHY|wC&c<)~V6i z0FwnWyeBIj1PiWe2~zlj)*QflxvkB(nz6`RF~3DcZEuiNVa#TJ!?Txw_V2@4=h-P` z^u_#GK&+qqXvg7auVuz>y0uw!YE)5cPcq7_PjW;D*Hg8&>#6lrQDUvMFN+Ov<&AnO z3s1=d+t>u7NqxAl&04-)rwyNCLvn0bnT3}5<+tL}wtn37qdzoovE|Z-39q-;eU~GB z%P(Oda;xq&;^$%j8+cJpW4U)vI9VcR^bk^B9^%kad8;^2me}ZVRoRMmJZLQ z4b38FGma@8(g~ehS?qJd6!+U#Mqq+NkQr}is10DBJ2y&UPBc@lqfRKw*C$uc9Y!Ob z%#vzPH+E9ns<*Z2F*L_epXeVq49kFwot&^9KCgY$_U{=fI?Z1jWO=9pFvJd%+kM>)>{JS&=5AcoJ>&f6*e$koF4mhK=>JV0b~RG6zpUF2q|#!t{S{YgZtf8=f`j3TlZ zGG)D^4mwa|pIbSma9*rjXox#$4svZjelfk9dSNJGYik57JAncG*kcE?0lWLO6$Ex{ z7qc^|$n~vK&xDSt%N|#(8^5YtbO%c=9GGszT8qkWcI&p1FL;xHkF}s>en$!?{GZAf z2dR*>{g0x^`lAURtUzszeQ=5G{|6#RIcT;o62@PDA^BLQNu(QsHf`3tAt{n3|P z|BMGrUT`p$9c_Kg!!biQ>^f2T;`*e8x@DH5j;CgNz`+ugEJMcjy~G@job9)PjLW|# z@08Ooo&;exhhy(dNBl{kYpp0^gGe{P=K__)I!)a#IsS`Jd;C?k?$Fj}cBvH*ryF=U zT~Ocrevwm>{L$5J!6b}DL_6zh>Gb49kn?O>j`L^=cLN|;qaLi<%8`U#r zm;U>K4xQpYztwwa4s3Aq42IA-3~k%T#rr6_Rx<0o96x3DiOp|qGvQ-Aue)r+FZEo&F z86)gA%y z1bPLG_g1Pn$9$)<%_sb_dG-A>_kQKCDR4oLt=uJD7tBoCLyBkN$T~Wgz&&VvV=jn~ z8khC=h-9EHH<8ZXcGU@bUZE4lt-No{+lPw{WXMjnW{zvyvgN@(IE?Iuzmh??i%aBxOG^eKI==v1d^vdES!6+9qMH4xfngml!+o1hdL+$qg zSD`cmUh4;k|7QCl=>Yx79T=r!*Bj8VGt`Vzfh~J5%Tb)(<+$!km141OgW}Ff3l~V} z1Y$a?!+bbbWk~NbrBp+c$w8B}VBd07Q(m`mbov_Eyyxj$hTdhE12hazcT)?_P{o}o zm71}WHom0&37Usd??W?A6_J#|Wgw7#s!Vg%A=`};#E=6C&E{oETCi(SG!J~k3A&ma zGi|@6h9nEMn)hE01AL1`a|G-P=(zN6JbypmZR)aQY8M`Mfn?goNI*}T`>6|OKL-wT zf$rnRMEib{=4bQeb2UqDkYT>RTf;WQ##<{OGJ?e64TiSlJIs!>2B>V@1s8=-LhHk0 zdGuu)Jy+$)F5fOog0|g53)edaKaha`@f%LRw-?`ScXnnphmWq@^(jUDNiw`fIxbyy zVluHtZ9SjUuCT*iYBZ&18wXx;1gxz4sq9Ao8+1d#0Vbk5k>Y=E*0?|vz+qEL?~}#E z){|@xP7u%BuxTh(HrdgfAs49addSp$Rp7LY4#@9f-k+Vax3YyzF`5OQDFxjvHFLQL zxPib&b)ldS3%u|S*mV`&MP%7e=nUi$Hjv92d`44c=E z7KI~;Af-`nK9`#{x~a_{1Ds+wTYV#(;2{CfI&gZO>#;K$;AODGmE-5;E{(rT?b0EB zv%&*9*4#^Pa*Fohy}lk+&Ac3KR9a>|?&B(R@KE0gbnvt?-x;9NG_Tik11Xlgc^Yyr zzTgeBUOg)qQx1x@~{o_KYPyxo=G_eBFxUPCJw5*3%02of&8c89i^+TSJ(Q_5ukUoVvNDC`4gCDmALpGpw0$~5Cl@NHTdpiYIabVe%kr@3a|$T5 z%8$MS(3oMn;6p_u;$@(1o`=Pmiyf9px7L0h+|;Gh z`e!W5Y3x;}UTWEYRvITueGY6rTV;Sjqhlw%DliLo!6$D#JU0OzO;Z=Y@EStv!1e9Xzl*dG5$j z+a$|!KeZHz3ocC(arsQA_^`#P0d%gHGGYJ(N@vi^p}HTZ!6< zNde$V&`xvP{qAyq;tCsPwM}1t4k{aDWLJ-BcjfVq@3H$Bpmk%`d8<=s>*zU~^ezYm z*51Om*w(o+aYAk55rxqqC(&V%>ji4O>dP*HxMyfJEhB9B=c;54yN&q@?)*zw;F%c! zXA%JP^6j|kcPzV<@;N_eo_5}SB`|2x6y)3v&tCel(#?#^FgQoyN4XZANk`UlnS27R zzk%_$C=Ys0MmVmPQOEj*Y4J~C#-Z{DAOoq)Kj04Gar|3KS|cIQL^HC;(#hf!#FJXP zckc#U30egw3sQ%7kF(%5WE~n?q{iwWjCM$c9HV^*($flv5LMIrs&hnN|1VMd@&Qf@ zHQroL(h}np@~wJGwBK%^gZ7Uw<_v*;9*uP^iQ@v@ztB7QG!Q9}ZKh6ofK^kp{Ry-E z#S`f_@kKnE9D;nN5FC%Z(E?7V4M}ZxZI9f#mfV&yp@&5FN1kk9K522Efq!>ImJLyw zP)*ByeUfRd%5@H_{Vx`Z!zR{nDEkJb+(Q$Id2!3@j@qj-B@D_2ZQ9K_R7OAe zdx7;?Z*t|2Oa4-IJDeauqlvfgdmGnG*0i7O4R`~hWd@sCU7#g|2=O*91lUexxEy;j{I#%!g%fthy;-%oR zk7n~DQ9$zLxM8hz7NZ*p*Vw8tsfFJ0a&H2*EB3M=rdCm4&O@tzD+LTY-}NquzsWz} z@n&ew(wDtg-H;`X-@IFykct9;!^ilH&VpoN7XJrTiNgYL`}@~Z(;F7+?GEcKEvM;~ zGPY3hf^G6b@Du%??gtUk0e?{oPO?I^&Px=g?scCo`G{y0F9m1;m)IS<-ZH30>F!j{ zIEL!1PZeX_(UiI2d*-kSw@V{M0Rv~6k$pM)3M<K+wPI_9+7Aj}jXlB6xD*r<6L&IwXN+`=G4qy51X| zwejZ>eE-4lSND=Ps96axrx=7ILobhrU-nCKO=rGortr+^O}&$T^i2oPPi7z(I;pS(6Nf2~)oR2{2hs#1E~lEQ9QYD)k@bsF4AE?bnMoqFnc_vxC%L z-ztoMsXlEbscc}0(Ly#+uZ#P z!rmGcI=yVN##-#NJZHq$Lv6fS^?^n*SMCMPI|-^59yR=DQTP$v%azx)=mcpMD%sjD z6U7|jx3#jYXELTnzte?6SsJqRTt;}V%!-L2k_7pv7XQ{rwC@q7*y(G(6XT&D=Pfeg zrEc%-=J->ocLMVhcyz z&bANN$J?0>?FlmEUcCy7Sx1C}4x|FnGzqAT((iiy0~2rlfeBT`bMXUn0(&ljyRD=) z_XL}>`7;jNdG;4P(+I~rUQW^a#j_b-EHXiLEU`z&yNxI|-~J4RXPbieECm0%y>b$r zwuI;a+!;N(2+v>c3EO*p__~V9!sn9v3pFJeZ2t zM&|)C4^QD{*ZmEiijg-#h-I~JxcRyH?L*#SOgTzBK54STR>F>!Qt+A!YqP zt2|!*55tu_uc^(9bLF|zOJd^Iqsmvx5!M#H-=o=IX}KZoqDmBdzAczz-`KEyK#LG# zG!nlfnS_<@@Fch8!Sx%Jy6otr64L!50cDol+WAZ!P;pcJO!3jo-e$&+!mBu?&sBn_ z#6|!dj~Truc;vz0fpe=5u{@p@b!@-ed3UsvYT#|PKm9vt?m&tRGee0n_;9vD_vvB6 zvF}kjQ@Te&bW+bUFT`{+hA)REvNlO-L{XM0vg?9azS*ivL zReWl_fkHYT58Fu3pNiifn{HCrv4}W(-?h8V>1!_5TB!lINp~q&oA>g;e}ACg*WhfX z1`wI@AA~wyj0#l&JAT)QuiSz=5z!%M>pf4$^qP83A+D#ZE71M`4Ro$1%iSMuQ+%zS zd#E*7*mH5L9mCNze^_(Ky!W0`Y;iws1+x4#u#R@_Bgn?+Rcdr@)0G2m75XyTJm+75 z6TwY9&JDA*wxLL-mG{yID7HVp?qvvvTspJg)3l27kF2M-Pr9o za@n5yuZT7Z*ImWCdRbWYO#Vn=9pE}2cf35uM0BrmAPvR7vfG(dV7rn>CAIs}Tviz` zr%YuOIf%mf={@jnn}BW3z~`7QimlyKiXPSnu`;@VtHB&%qAns*Q;?r|m}o!pV>lMs zcfFfNX?U}2z7XZKgo?CS?M50Y12;^*>zO7bUUL$f=op!dpj|69 z8B!-nzorn$c;0Y)u~-yq{2v&~rghZAT z^*mwHka8kEw*&Pm0d+(f#a&StNVXJKqUiUZG0wG=aW z)-Rf9UR)q;*PzuqfA&Sy9ogXLq-R{22gV{rfkf_8PRj4W)fhiVWAP>jc93Os%$qfK zNYZEmV=*=aY>IV9?YATU)_iup@8g5=@QE=9VGTl94nMG}&!1T)3y$NHO9)C>%k)u_ zyX&b6-zv!zW$%(URj`rHp*FKq@s*g^PEa3m+2CidkgCveLETaV(~8Srh3{szWV6eB zqf-8~FY9~BLj|COE6)JU*ml3>=6IDPqEv@=$G#zpb<99#DxAco{@I0l_w_}YemBnv zPxBGi&Lx4&`)-4Vx;^0oYl%4{ig^5U_MQK7GPIQXd?*6RVO8MQVEx!z`1 z?UcuCC(tFB-ckCqtOp(X z=G^!`P`q?EsLTtgJQ=Ff8|%nFLXCMmr?Q%aT5j4z!D2BoY)!&|nS||+uf4yuwso;I zn)Eu=l0Aq*$+K)af9*F*nCy&GPAsyK@w0Gh*M3+GwBnkkfZ#jJuWHI0&5l$cVnoIBNaN>psT(%d0~F4nx3x1IZ+R+!tYMxz;e zOhL0i^)zN+Bu!^wV`ba0^X4SRod1D;GxlQ0g$d7_v2&WOd=cIx1YKK}%*!?9Xa93G z&IPXq>+O3D{gO)0w}38rYIsb_)~3Y2m+mJG{9RK=;;zoNa5<}D%CtXYRld6TyX46E z=A(=`VWat=M@Q3(6c`7RZ{4h6=;yXzRg(S46DSHY=RcfsHqyL&d0KlJe5WG3UhSAh z<85cc9EMsrVUx;PQD`T0WZe&W=OM~cvslFpHo~@A<6W{6tWTf|p;%>Wz}lyUUJ+=U z6nPctYpnC9krk*w^g?hyMa8AEm-P*}{w&hj--x;$j4|s>+1wiU0B81w zR`}N(0T+T`ER&oQ5Uk7(sVj+GNTtyMHViC>+~F+QgVHvN4$G9&V7mIqS{7wuN3zvn zRp&LahWIJYpzQ{tN=&~y6|0->O7Pt#WbJFQQq>_>Sr(Z>d+R^!5zkPTu(yC+2%|*F zbqKRWB{>F}oUx~}U02klF#at)r|k3fG`{omD0A}+C!wr@)l~Q-N}ALH)rdgA$Vgv; z$EdjQ^X;ZtMcwR~W9?XU%2@lIhN#n!gzRQCk@iyETBskLiL^(e$w`xfGVtiDwp)7y zXDpAEUYYv__R+puVC(%js`D7t$wTj&8QL3bRw|UA+QY0YaSvy@L>EPwiAFYkPMV78 z{jKzyS%p=i4QV?P2r#_rdJ$s804hZi= zQy4njh|&4TM>4bvE@E@t2m@MR1#5+ zqeM<(d>KrVf}qa5#;G~K|)zrre2###rTad zT3z@NeaJ9xn0^+O+;owpsDKA{LpK^Tgf2~Q<7EEiFqJmjhD)AR^n_nvtNiBi;%MlS z#Cf`MV9=|fJ^T2KJ~}yCNaP{OVlr5!fVFm}P6#!A%!cK(pfCR^g3Z8~^FbhC{! zyupq1HMkSw?ry}z?9f2d6I9bnNt640L%Y9nD!5w{dE|JHB48tBk;ir5)wET3VJZ%_ z+xi9+>*6ly^B{ggbRpsEVmteWu3`2Mz0iI&-{tg?X`qSwXn7cl8?8}MGvIi1Y0KAi z(>k|olM7ugKqNAAh1Ru#wr(g3^)|;js=S$ts-wOfp zWMyimfvOADxLdE*#7VE!C&4j_9?L~ar^wrds>qOthb)#Gdti{BL4&ZgPtnT6DwyHh z2fT|T_ENJ)IIVjUnG3NJi&CJG%e%tIPC+%sFe2qLj~2chm*9MPym(l9R5h4)Gg$uD z3()Z-Xn=p(9CGB@#mc#6c$(J22BKQZVpmY^*e2O;pA~hsECF2S1WOCG?VY|2xP{k` zXjzKfII3L!7~0bhjU=I6DQz^DYRl4l*V$DG9g3?uIjt|s30F6%Yq9O${P#OzY@aL^ z4-@+Z5QbawFQ zH9`MgIew8;izs-it!$`NSByaudquIoT|oBv3Z?-&_)cKDjsFL3~ z+T9Aq)JQw&X-`pM(~>t=%BBKcFQ|!az$%B(`|0mxpY+S*WB4(wyT(vV5{klStzlb( z{snTMGv%}VP$X0jF>H*Ypdmpra#1m23GddH)xjRgow8u>vi;F^dp!;9uZ8Lhe*ptv zfPSNUvFf1u6P;l1mmXRFiN{YHYrq#jqcH9Ur;kqLLf6Owo3-0n%PoWU?@p7M#r(b& zN0ZYz40q(0x4qzxu35#9eLIz1+kaM{HJ6t|zpR%d1P}ad_{=1-lBA8OP0CnO)seEidieHk>rqS& zc~{J@A7C}rY`>}X<+^Fo6m0zeRbPEGs(i5%qRW(hO99%F9q zdDM3v9`AWI=MkeZsH%e@gTl*+#;~9*J#cB$*z0gl*YxpCi2?&3>nNo5nwk5K)UUyy zB9kpkM1dYrke6Pq8A<+)oaBQlj&^xcN70g7C_;NjX5m_!^G?JVG{NtX{N8Rtn@1y8 z!XsHJm63G{lBZB*Q(hfv+j3mb159&;3oyIm`9Bc2zR7OH3Z3jlY{I2AwtuL~8v?y? zBXFB7Z|8Bq+h^VSGd5$;=m z9RAX@{Bogc!{2aJ?7Xbs0i8HJVn}Q40AmQS9=7fMs4<tP04f{QOyB;|Lri8V7{Vqx^ZQxaL_rBEd zMt`SZH5=dmsU+Fcd{A2E-Tf9nc`O+9-SXS?4@Fb?3XMg^7K`~d;ca^m4J!v=T9_DA-fQdhXHzY@G!k~!Pz}CNn)mx## z6;$b#6cB1-HYM@ijCpS{ttCok@fOiQQF-M~#L8)gmT24>c|n;)W_3|Q<7KX9vihdf z`9i9|?0|q~D=Gy$9!S;{Q9i#~*V(+1kC`BSU{GOmmTBOPFTsQR(*a=qTl0p$pv7__ zzpi`|o_1if{?0ccL`_gL(~Ql;V~zW$BadeZ@QT6GAZklf_9TsH;(C%0)H7J5<7S`cl2uyAe4U{7SA3KyeJJp2i}et)Q@7x#!G+%^y!;j zv~?O>Xz3EKhxcRu)Q;M>ev}4G8Zl;YOT?IPI2AJ5CjSa61VtUs={kr2H!-=6`v?s6 zysw5P9S0qUkAx>Tk4UyauZ3hAJ`7%ASQsw~`uHjzt~x(=k(q_C{)ryj?nyI9#$Rbj z%05If8Bk(PHF?>iBlGKWdM~w7F8m66!L`IagIy`Hyf$~^?fX$ZSGtY2EEr;owvM3X zvs<>wikrj}&DpKZ2a2?W9(ctY8cWKAjh#VVK_8dG=D+kQHg|Ciw;xePaHC#HNv@QM zUI7D9@VQqw^2XS6Bcf8lV?~Pm_B0H3Z0?NLgh%6&qn~N@ zDQ!1&8tgYy6`$N|n)D85m2`*x5oB8Rd^fDWtxz=R$r(_^{-d6hw_Pv7;KJ@5=iW+7 z!%K|KN@bhaaFbNpis>f;-^RRv*KPV@yQ|qV`I)4SmV6YvG&X}p4$ou2U4oVs-Aa!i zk8Pu2%&&QrTHo8aj}ur(s1xM*P1F2bG`e)ZndI>wIKLV z&&GG-_WPJEmAN4f85DH!r2Wg%d?RQ&jcw6~2VoF*x)n39lsX`WGBWzlOI$rwcCy*P zucP&qbTd=E2dfEVgZ<{szR8(w#1TIm4YkJ4gA1Fki+6ZlLr=e2$D`GEhXA|nY@_+pr*Ck#7H)6j2b>71ozn3FE1LpFT~zTAH#jIky<+g-O) zG*hJr^rtC+NZN&ZohvradPMLp{{mZ0t<`2Nn8lpm#EyYA!)Q)hIcF!leTu){4Vsn2 zAhdZhYp36M)K$-AZdwJhITTbT@QrOx0Mq)9b^^?rQa?bDWGa&?_IeehG{F21N%vOiNp0x<5?W?kclc9&$fbFb==PGMT%!+s^zeZ<6nY zWcluKzc=bs6$Wv*@4LU=T)o&KwBLm)mT+W=&e)w;b5+@h)e>opEY}cC>62vaC_h3@ zN{e4?k8J~C#p}SHAlAG+Wq6aj^I!)gmXB(ma@e+T&TjMx zYE^lT@j zXD=1pte2+m4r)oUt#GFQme?4>j*n7+^?jwqgW!3CQ~?1;oZAoHxacUaos{=)IGL|D zF`NFSx_X@;j}T!bEoE*8Mg|mFio0O)%Jtk+R4Brg#G=Qnr(LPl>HaHDByus#MLsrO8|19lwX#t& zDtI`J9Zoo*Rxij7JCULA+ZyWunB65Z3*i-7nb|m`5g^=ihEvE#_?))QF=LCzkRKzr z*e}K@+q$JF%8~gUEID)>Z+2FxKhZdGh{h0dQ124z7`P@92$g;R`&lgQ?OgY_P*M$K zE1V86XX$-=ygC&VKpzMfBUdYNT4ZVsQqomb3*~=ych%!GB@xs2l9QyvJAysftNaz+ zTSQ-DH6o=UR6FtgW@-AfVGrucF@9nSY3lXeCRMVIar~k#^T$(RNsxB5K@pOrSsb7M zK2ESrT#(qklMDg}G@5K`+0RXn{#l06aro0|-kBB%8+z~4-NBnzkER&TvQJ^(^P4OR zrVS?mb zq2JlxD*hUta3V91ZmU|}aU%RR&voc{npH_0-zYOr;}<8XFy7Ym+E|t7{}a2779@85 zbHD3a_2Em|JEw4vp&BmAA>heGPo#&fw(AMrifR=Rl}y0=5ijF5kv*%FqOufJPhoec zv>((XY>f7PQF!WkpCDPde|_Rhj(yve25{qT*;k7!r_jek!arX zMf9%lB~}TG@9)oVNbkCNijtT#SkVo2kbb=+*DQXAH+bxxJH79jpWRozEILdTwbRP7 z5~$Kl@|Bt~=&{8f|IGOsYhvU*K@Fcbke#zHwv)@KV z(~(-l93!k@s-U%2%>1MwyWw}UvOCZLZ($9``HA>kU)GNvK(Ot;en4BF2VsjqR87;%kg7jiW3SP+jj>AcC$7g zm>ORNC$?e1A*YrhB3d>2Te@nt+pM-ICJIwzY_|L2;F6gM>rYbZ+~sZWoknB#HE74n zlS7C0YszYlc{myrI;^!{@tOCceln60GQF}n!OV)33u=XzB2RD8ZL7wejb3^ck$efq zi4SY@&b)ds`EKd7Rwx5@w&Aq;CP>}Ig`575=ytMN>pJgMCJ}x1#)3ue@i||bKa#qF zbJrUJ2;43sW;l0V@Xp-XeS~LDz0Sn!XI_m3W#~L-V4f(a)q>|{49WDD;*MYdvel>UZXfze@?uWkAJ+CiJ!l-_wF#TlXT$`ZY}ij<*^ za51&zc{&E+_1jw|DMR2>o!I$S74WHy)O#;))!qP=W^s|i|29ulsJBOwz>fA@-2UNm zpG(G_2J#Rj&OoHqElVt@&->E2En{lLD z2pZ1m0-Nu0d``|&5;5_8^HB@&*wKePOl9+ai{l|4`Mo5>-ddPvVvGV*@|}LP*0@~fg^?%H#0gJ& z#0vV%n&EC?bR3@YXAyT(Gr4fb>YX0?WaL^lvgb}%Ou)i7-eXe4Ftd(Wk~XnNq7$Sr z-0NNtUtw2&1r>l*i*c$Lay-Qie9xuUQqw=;#A2lpJ;ICeBbASzHpyp_w@SnE0GKm_ zdd|$!S*e|PL|`g5Kjy-O5n&wZw-w{<9EQHa&@fH)S>Nr1Lv4;{C!@_rZWYx#Zm|jB z-6)CN5Ib(oTfJu^Z(@37UY4F;zVHVoBQKySyZj(DRFd|zGSS_nG~O;oq-mMhrq7^2 z#7%hL_}uNoyGZ_;T>b^(B*WFgIflNX0B5V(d?2DLlJkw3zx62*OP~#orE5d%ic}czW-Lh&8DdK84PJQdX>P zDPOj)Bk_@v$F&lpUguTTzQ;xc!#!49#hFuQ|2GIbR})iWsMVgp`K~;l&O)~0ET%p;~Q9LGtm#3XOeD8L&5JsXqnpY*W8*?BB5BvQ_4ake1-fNQH&+N zoO?B)>N=oGSIbMvGk$JDfJh}q{-%r>VIvf8UnqhyeE1+{n(yr5icr(?=QI8fnREs_ zer~i>?TED9UI}ul$6-rn9xYfZdI9@d2xR8VZRm!jY^?98OfD_~W@f*1cJXVY*$5+! z8WB?eMXGHDA!5E$rW~iS_hEWt;xCp>@J-u@OXAba9;HKeh5?v@?rlVgFNo5LjmcuL zfu`X*`WJ*)ibm~TVWN?{7~a!59j0mCW2?ygH; z!T#40aZvtxLB~~_?_IbJbi#APU&f3PDHA72Z{D&gi5{5l@VyW0XB&~poV+$1N zx&yMfCsPfMI#fqX2xRY#i0S>u`{mKh(Ub3VHdx7AYpX=}fVo|Vqn$}ClXn_KcLi$p z?fak!+06x>2qC*q>2dB_lhl&XBPDaa-MG}8*RRo&(1s)YI{2`%#d{K;FMT^v<|#A7;sD>u zgg6it&F($SdzfRu(}zhvl9e`Fyql75J^!_8-s!@HN5*4<( zkSQ)xkRM8)pB^46u8UmN<&WroC#&n!KPD-=xD(>nNTk&C|Is({U^0KTV~viW$d;bY zA?Rr@5Lg$WAw&BWOM9I&lp`$(xrx;0`RdnUhByA>QG zI1iZIna_@{pw^jmEaCQ2tzZtxq({-{-R#OiOeofGJwrx7;E78k8xk`Wnq#s zUcVQ8_(vNo(cAA;wnFQ{?;aE4 zy;9rvIgBXmGe^y%^^W!u0q1!CAI-v(nhTr~bL5#sRpdg;``KJ+-0HO%aY{WiqYnuw z?4%}YmCt#kVBVs!JPJyRqeOEn;svVqb?v0S#9rsEYK@KZj(s{-iT9xzS;gK2qIK^=(wb9@HKy=EEw zb)fYLShY+yawms2Tk(yB#Jy4{4^4G}o{mN?@ioBhi%AJ9zqp2FG(VH0S~VRO3&Rt6 zs*&2J3SLggQ2rf*Fcb)YnkYmTs9|t0b1S2#2_tnoPb0+AS=FKeN>p#^Fg3h8qZHyc zInL%>RNY=Fkb~(}T|QBgk`PMVs>z5E#WKqc(8&9{Ef>Ww;wr28C&o6+u~Qdnrr-r#U0TlfQOEm%WZwRrjSfW36I)LYNq_~ z_Yi#)i%KVG__rNcn!Mib=TfUfBxs!az1sKq0==uB1{mD}Z!y9s#%{|}(GT;7x=V)i zS(*U)60eZD>py2t&s5ds6+{wjvZgRLrKZRgdlie*?My#9=iW^VPmE+A{xWC07rM#3 zrd2}O`x(|Hlxe_U`J*7&QEp;u0{P3!r3!K*_qr{3mL73hY7sTJ*(>pMCd+b$ezGo! znI_r{pfIYvA-Vu}ccyTBh!T_x7>(@$6`;&#j}C*@W>IGiL{wos#=R(pt67&n=N#zl z5(8McIQLk)`Rnk+SapkzfZ+n8#C}Lkt)ou|t6B<`Tof1! zfh=BtIqKqeo+YMdc0|vP@~1*8VV%pT6FBKT!iMd{T|oJj=jMp*9GrbN6Tjy*;7fu1d~Q?OijX9HZp zAA&O@b2s*os|tbeVNAyPg*7-piMCkxPm#jxy#QG7vHI4(eX+E2B;0UO!X?JZvLZzP zQ6(3+!LAYGkVIXd_Ac<#Fa51%*0;?zl}-H=;Ug?;a36J>Vew8XgDXSqa?<^ZB`MB+Hkre-obO zFX>W7tEHB08OkiriTddn624Jv*Mn|eE&t!Jl1|orICYylNgTya5~*NEw8d(H3lIkn z{XDZ;630ztoHUoUp7)c8(oeDT;*BF3LhJOb$P5Et6$7*?uakpe%3xbYX7hIqk|=wU8P|b9 zfQ@-ce0#hgKt{G*#HXri%V_r0>Q4ee3DVlfWSM5e#vA#{h}yEGA9DTMnRf0*OS5b_*b<$TnxbfNmLFxeDu^#dq6 znRu%GF0ob(3s09!$_UO8KUk1kKDZ&2vR@m zJ{zG-QzOC!pjO+Cr|V}*i!9KaX#>VXFcH#GAToJ8U8EVfg6jjNCl?}oV7YOEpnt0TI03rq<> z1)2qvV7D_k$S0>6`Y$Z`7A*g{2OWXSid~D10I%=4`Srag%Mj$G>#)pZC-ciCUL83t zvn)5cSa_|65Ij1(wRNeB>V)ei%y5-Wi<*lco@d}+#2uSX7Rnij3T?6`^i7usPgaLF zH{NbUL0MalHU{%6N4c5V0X1*nrqt^1ztFG=S!Jq)z54oQz{ORs!-|GPhQg%q?e=Vs)uvVMK6g0=aC%0{7eRcf4HqSpBI`bvvs6N7GeCRn;`yM?|Hi8v*HV zq@=r%?(Xg`>F)0C4r!3?ZdAHErTylFsv-&bboMfAdDVf+`iweO)BQ>fLPXcyOo%XS@$MweAz@-fR(zSFxz_Yl z)-=`LYxIZxsud!p$D!F39?6EE?r6Lmj#0d$nxd7rEV=2(D@ z*c`kaLNlw^=tTT0+t@qtE>20!w!9BQ5r(*#=t|`#pA9%(r+pv+otivVMQUr!323A- z6MvXShYaRPYR1ZUV*!3PPI-9(=rb112h%@oz4D0bm7=nSfvx=1P$9~YqkRmm^XsTT zKZg_$zzQg0biHqr_-gTf;S@qt`4xbRI`tnwLB<={91=Jw?Q})BK@wHAAzAJ2{UWP#iT3%OmtM?p40hTOVe1K`aX7_PB zhVR({2Uj!`$$~w?lIuP^Ze{fcNzhif)8z>1 z6fp#+HB3eY#bl=Eb7ajWd^AX7U+0CKZ_m(8^$^l=Dw128Bzh!1Y&m>wT~*j z%z1T(b@Y~1P08zN+!phc-78R0B=lP4)cUg%T+}9Gu^&M6JC5t`FUOcKAJmMKl%IoP zzD^D&7g&~*o2d)-(*t5aI1RV-eTW=6EtfeHqU`B>H|sv>d@qa1VTIIDZ$OkW@;_>uN=GoETi3{QD57+v*gQ!@90oZqr z9PeRIuz&pVIOhDuM5h`iP!k~w+Yi~&D{8vh)}7{m*y{dEjGi7gaJQ>*sZ6AMn=Ol9 zPWSQfxIb2ooY~B-5lQ1oP7s#O7N^mNdSvsS3E=MQdHA7PjpiLUp1-g^E#TEFpAetY z$BbiU`?$A5SER+58zMe8yzF}vA$)2>720YrnT*A0(3szS>##h!TTn$3A$zzrVyDGx z0-yBH94D~mmXZoz+rvOT-7-b(^KDYG4Qy4OV3~8|XdGV_&Nb|wnbQgxqJ960gA=EG zjZNli^bvPIbQ9CU#)X7$Pg*4xw&qifO=sQwzOdi5Nd(^B$$nk=)voXH!2;!zb z$Mi-0aCbHI$?hYq~4xh<#S6uz>H|JE>c!~y9_5zKh)>igGGwItIuZ_7{mfmGX zQIViryL&_A<6o@}H|Oi!uemm4Y$76N{_ui+0@%Ntf3OCkJc-~-&2iwB!hdIG^Pi3v zUQ0x6vaq}qoI{&@_j1;LesrwYWGp4~CL*`u0qlfp0I!Cp-j|Gqn*e4mG>5oGI?0=pX8X*`(f|IO`;b-{NJ|+F@4l?^{CpZ z(^@WK)5*%TC*pNw-*z&HO=LDrxv?K2Fl{fK(-^Ukw;?DFox!di2kt%-(rB-*~W;@wCa4JbV(t9)O!!l zrezcVvtLS0V97{g;9mBZDY|x7T5;K&(vb z^0Sv!{WP1JBFb74-f4h4$Sf$cyC6%-IvV4`zKbfrP0yP|>oCQ(`UO8LfPQIqhFG=E zmvy}GnF@#WV=9!MTt0Up#&+SbhLnRJ-9uU8+)w^Fp@8SV-T$4X+Y>{Jl2U!!o>*o$+pYVyI}HCdrYm5 zT;1pE*Y7^@m;Am7HLRRjS_e^*J0wc8iCldB-d7_OPZ?{=zdQgyn0kk$Kk0yVcze7g z*Su;$!+Fv3_65bWg(RAz_wQRXvcq<}e~(MuMj(9nm;F8l#L2WS$$l0D9edF%%~22! z87B*Hqftl1vr@w{I?@+#`OLK*m~!^RZGQqjR|I?x`Ih(L;cdXDaDRMNnyddoH4Hp+j;1v^p{TVhhm}V5zpD3$sl))opCZ>&KGlCE7G4f2Sq75SMQ&e;quAy0*ZaM zM_0NWcIC!oC>YPX|8*^G*v(^J9TMzMG?Q6jsr+ZsPntD^}VY{2|cVqLcfuFkxp9&o&W)hHI=&fanPd zD`JK>LkQ_1W(>K1K%%}gQ4k72x;31J_#mH3DRR7K`GBMEr4t9yki{D(`Z|4z@1K{i zrWR@zokU8DQfa6pX6sz)8pVBKuglT=$ojqDySX%{u!I~EE&?Ua)&2EjTPkegE=v6zkLw{j1`>r3avdOQfQ z9jM!a7jS*K|Fwa;e0f=ul!YKT93D42@7g#LF^_i>V4yYE|DDwKA!U;q@b>Lo0>w2R zl51-zg4;!0TK=XRFNryli85$lDfM=x5TE3}0FB8aFqu3Lg;3IDNo8aGz)@R27#mLNut;xT%=`O4jUi~Dv z$t{gOBCsM_%Z%243{_r%Tfmd!HZo;7&#S$-px}FdY zl1dWm!z~xUUgZH5c}H7yn_!PnMG%fV2z(`IV&vwTV|Z=BkEvRx$aLfhZ#p9b?7Tj1 z8(I~`w7uM|?Ut2fUnxLd^Ve&Ti@OMn+rA@eUea=mqKJfWy#J)@S$fsBX7l3twNg17 zNW;?!Cq7`{7z~aZPUi}A1r!EQm5}(H5cB?fc4T-L6zS7ss~%~;zB-(nt|9hWD@ThrIsQ6B)R4mQ+uC& zywmA|1lT`@u(8)u)AE-YQL>rc5380n4+62S`9`d=dj>E0^@mS|1A1eA5GSHLVqmi) zx%u!A(L|gaGAWy8STSLU#5_X^!i*iR>p{-3_K`@n>E=`GY$(l_=iUoFVcpO@3^W{8 z!E}150!a3AowSe!TEQy6`+5Ww5ig1C%$?ZhhGlhVZs{N};}OQc1LU5+9!{{KFpx$$ z1m{JH#eAZJ2#>VD-FLf3AZ_6$ntkxaaUsZIbTPH1Oi^!&0*&U?_H_h4S~JrE zjPe0Y?8G~9e92)KA;Qf337E`Z$R`A^HK>^v<$3f!;ljb21O+NsMZn^zezNzmw;y~6 zIuD*I%h7nR5v`v^Mmtq@zRb#;z>sn%#@iYfb{8xf&@7_%s*GF9adVLOu_( z`@aQ`ErbqtJhEwCv|jBmYpvFrELG)BaID-@T*40##++teDOGprM~jRDhPDi$3>GBLi1joPzm7v`dVC#QWj#x7_g5424XqRG4u} z0Ro3Gp*f>bxG$i@;W~%olh7&GoRy<8#JI!DC_0VF@AZ%ghlh+$)IPZ7p(U@IuVd z;bul`jD5{&yNSQG(XEw{vY#)<3qcKyTsl?=iL+mB^x$o2%y0-T_AodWK4#0Kuf0;C z$#6gGgpNzV3FU-xzQL^mN79juX!&e}-c0RwupZw_SeFirgt({v5bWFSB_R#5*LUNE zR7=SG&+x)lc$1e%=gotA)`!GBzE2T`D|&?JdlLaD+Z5e*b37!rQ`uj?ZWf&e-Vf== zq4SMqzW%%4j`@2EH}J+fHVp3p9JsunXIYKkQrUz+V;QC9!c_p6cKR5o9!<6Yk=3$|J`tE0k zb(6UbqJQ{`!6u15gcD478MW5Z0fuwi5S4I*&Is!rRES%HQ-)~B9Wvf5V8}e;2jjj- zi!*9c74<>y*@CvkT8*BT$w6)#ub(+rrU+1SM?d>M@|{q)r_9D)^qLvBe47 zDKIf=H9=&(Y5h>>zbGVJuJ=~#DGG~bcbw_pJ^Y#r<>}1J9BWPyt)bZa&2nQACq8@o zo#oOwx0#2K!Gn6uoprSw~WBAS>zjTq1WU)ku(+D7|{BMW--S@1gBua z1UAKMV<;_S=S`I&FW!Q|9F3UUDolGjF>5R>IXj+hN8M7tvWJ(2#Ve&Nt5+qxf^Vwb z^dG^JZS7?W;bT?B9e$GN!B`2oUv6uOEeTiED1)de!a@YNBO(&UVIrv{Tx8YAWL##+ zKB+T?+`=I!^tMTg(GEjZ4+wlCj!qI%untY3!majZNS9(`T=Y{(R)+eI_Voh6%RMtI zIIG)E?*#h>M|3NE?C)b$AIXx>7sv=QO1;cl4lUI%2h4r(5LHhd=%D{;>6g=acHJnO zBp380v9Q7Bc8B0&GV|_Bm9+9TWavn0y>0yVjp@PAj{o{6;d&pO6dA)0MT0`~I?qI7 zm=jw&C#xp@>|w%X3mp!&oZqG%*%^B*2#Pi*bgx%LfIf@XD8WHrR*#6PUNqcCy_cZD|KbjSI6AlC)lzdbei6RoIUU(9& zN$=PYH*3-dEE=@>p_M)idPO-G|y|8;PnzghQv7+_qGPP zQt%;8aU{>61}&{{lrTkXK$F6Vm?;|G)+b5`5WRRInn!=ak9!&mq!fo@Ki;?Spmr@@ zrF{(8yNPr#_S5q-NkChD=R&$w5}?a=8CnrDtVNU1SgcpE9OzaEn_;$6AOCY_{xQ9H z9?tX2A%qW~l`4h4GT$FGJvjJf5kwo$FdYak{`y?)yoQ9AIJ_3oR4RI{R*goUMY3idw*0wLUz7~TkH-x)xT+%FCV3uOt!sXu z3tm&2=8Wy-u1x#4;b2%cjF-p5zc*#aw?loSnt52KAd!YpR*Z(ahQF$7NWBegb6YhI zeMNaMpGkZZsiOPRT4kWODooFd{cqTdD^IfaqJaC~ME(__oGXK(-NQQWaS0B3Q?E~2 zSMEl8I~yBe`R3*$L-eBl7YDZ|sKUBcr1;RxllVPPRHJfs@V+bMYXsc$620;&f%LbJ1 zZhY&i5dR2thPd4?HGjjDa@3DsWvO8M11k7Ams0|EqF7C2#2VK(;lylA4dzgNC4OsX zR$#n<0M;w7$<7|v*G%(x$2{X#b1^VFt5UW64^3#>jqWxXRJS94--RpG>EUO$1IMQl z?N_N%2N?O3+xw5n%$)dmP~V%wg+e*|@k&fzTq zufWw;sKZWz=%H0o&{&aMq*Ex>(4R44!{y^|GVO|lOReQp-%}F3p`&>poF{yX4-*52 z&zlF+^WB(w(?zDdt#$kaSsgAHO|f!geOA!ZlKOnXm@#;mcfTCvYV3E^$llu=wb1*j zz<-(iDpE0ZO4zbj?Vta3_S{bG%c@AVN?)cQjgq}b9{=nbo%1<>fwv!`g)o`8TY%X0 zl4LJ!b|YS6vA`|0giOf>dK+ok@?oPHxEyN+aO~!Fn9}#5v0}nhh@cXw3ZXQ$Sa~3z z7m{h4?e_I;1?R0mf;laBOj#nh$Tp*&y&f=eyR@QOO(T{g%*(@}Jt9v8C$gZh$z2Y= zb@@L>oU^?FftGm5N@aT1W!tWY;_hH1Zt3b^5;=~nO0|D;!{G)sWT-&m4WmuIf=QZ3 zYDsL3vLKUgP?K~@z*wr)?=u1+>i7U8u2P*^;PI7db3H#hzgJd`63{#*-U&T_zRY4* zLv)XqcL)zyO{rH8B>Qze#kY?3!elr&3~53Vk#W#1s<)4vgG3OPr5Ku*4IK8fTp#Hz-gVWy!~;+4x3B`|;hr?C`UCt&m+EBN1zD^su(R`l z15-D&_te+F!@!JpDAEoz?~cLf-QR%P`4u8lK7ued^-B|BWniXTo{-9tUFY^>rQy%I zxM7oJc9AYM^ht3FxTp;`rFMaRPlzC!D-iHZ!LBTEmKYJFBtbl;UXtk$&W~FIcukL} zXy;!8pVNO`lvfD}9$obKzb#TM0ji`DmcPL{$T&~gE+EU7 zdWT9Y@3Sy!)i@&xT41F{K1a}QtxI3c%K58j&|pjH0N0^dXR>+qe|vEj&`!uITLUxX z%UyL3ep#1VD5X>JjCzh}Fk}16hitAT{ocS+mPoJN=El*>TMDGnTa97TJDK4cXUUPz z%@Oz?`$f5}EQupF1B+WzT6eo$kb&NPnrJ-3SvDX`7fsavfPt9Y8H)|w$kXRQ!0XjQ z@u{t2ghh*^N0s?)o|RcXM}YmqZpz=!tbZS|C%UtBm)l*>A3sZ4tfQ;`J?sAG)6F_j zXna_dB0FzX6z}|Jw%i*8yBB?Mmi9rThnVjY6#5L0i0_i%C%TUfD4{g#9NiXrm=t2) z!sN*=-h9klWj-2=g}}#}_rE~R(5riVW1oUnqjX>WE_cHBcbIBDA3k2=sD8+|U$qq? zq5~YYw3Gy!z@(YjM6O7QHr=TdT7CTAg<2Mhm=?P^p`z2}D>%!K8Ik;IfAozcN|(Sb z@ZP%KcaHp5$niZ^yiQ5ZbHNdQPOv^U;R+#Ta_+Ndctv!WjtIDD6_jTS3hjv1XpM2j zrelftxfq&rQiRo3BYvH%S~8p^$zjY8g-l&CBP#YNdD*TCsa~U^W>a>^4MEjc}KCdZQ%HK}G;E zi1UwfkXrzw{AGJlg79c+W)zT%ilx)O5gWzlNYl$?CY#SXB9D$-YM&&U?Egr^C+*yk z?kXk%^16%w1KD}G3*BTe108`AvHbq1RM#^WYE6&?Ih4M83_j`b4~Q&RKRS5;+xMz-vga{r(sk8r z+T;&5$5qOq?+z#grTN|P;iQ}X`R?JbvMu-II&m=lNw%4~aWl!JZkhP@K4cQK#&Llh znRdOfuV?3gfa&}rOe)pW_cXD@pDEnQ!eXPGWtuI?Uq1QA&@FgPo`u)i~$BkM-IAt=v55I+GVs#H& z3rrGzmLV6)Wdak>3c-TPlVY7H>0EuZKFzT#hG%Old1o$JEWwo>%Kk08T!Q?chgIb+ z(}^;#ZM)EGDQa^2TUwbb3Jlc*npT))`oo=M8FkXWMH8?lB>^_?_fSsZ#2cB3+Pxql zCkWv=S~v9RxIoEZX8!*4#fUg}6(7mFYcm*c2e2S9Sd8Xq0h%_gBJ3+hXx&n{s#4a~ zWo&{Hs*2lXi%&C%&NJ%^@nf@W(4+o-r}6Y3%3-4&L0MYj+3)sbZ5(`PDnV|MvncsNA|fursg5cMqPKxtz&? zk}oco6Sb@RIeh)uVedS(;B;Xt>=ma4pvXQ(K+UGt2wvN6rc*$H;F$F2SDUgzObI2r zy|Of=nfwNv#lsXG4+#($J&#%VIk50IAI+>6UjNYy6PO~_IL}&b&H_L8?*p5}4b7*` zuNx9W6p1<9g;SjS_}xc@fCy}!I2gI;u=qCKR=j(k3Z3kI6jH!Q!7FPgAqS9JbX-)~X0l zf|qPo2KP=NgmHo#Z|f_P`EL$nJPzaR^b`OZ)bG{-#yH52@Iu0_v@~~=8x1e2-1=5M zS0b09Y1_$G=PI!d)XRD>#$zBdrPBSTqB6IT@~ZKUPZA+6=lrpxTTuy`yqs8`5xgz% z!R72wkgBski=s`|O~Qq&3;-H*R{~yl77#MJX2t52QEhk=K^886*WmUooCIR%Sxssm zSy<>ZsA|(`A1-O-LB;FTUkijbOK#Ql08^@4qdhUB1pg&ZU zIxWNjapWTL55#{!xAghgdpxU+sWpTVBd@cGC1i05FlKY=Hi(i0E*h(x4O;n9ya+~Z zRwey$;&Y;(gyKV=cO<^tTxecbyEjaB{47sM|8J&V^=ASzieOjmMA7?YI2hdKzB0uU zII%g}mW}VcyN!)AEmuvjg72tf$Q0Nhn-k*W6$SoM-VA4f_clAav-$bX*3OVlB$=ei zb5s1()l%wB5whx?1QC87g4e#D1x%dm{46^Yjo3seO$kT0L*egv4k1p?FY$aTEb+|h z<`%Cf-alika+{Q%;PzP=HJgL%VR$;oFHbon$vg8 z#_NW3W8%N(vD@I!Avh`M=|PyCQ)zqHU8v6i)to$__wWkR(uXA&<^GCIeVvHMkHmAq zzeYyTLvY8|sQpNC+Aur)(VSoo1ChM={&J1{ZIZcavg9rbv^Iz#6I2YKaRPBp1ytkE~Ko>^l$w4nU>f*Ej?B6NnCwquu6o2~${95RHlT zV8m!MR~%*4vKf2@$gBr~GBYKdTws1MpD}cCQ?=$OxLiKJAfwF^J3z6RJ`ig5~*@`3Stl(58Qjnubv2cYXE@CLkgfB%>#@Ojg% zlU8GbX;u;Bl{U99nQFx0iE@eyU8mjd;v;X2Q)4=nT|Y`);U=+689x|JpFRbq?JtI= z1UaklT0@9UV2b4f&zgfm8ja4of{zp;FORp^t0K+|^ro{VHq0oRUY7$eM&wlW%C(_a z|N5^gOM-hXg;85-SqTXY6U1bPS`|Kt~|v zWjApot*U`$pio3Q=BWtPW!XZDQ_o&P{cg;8?0|)wkJ9^9LNr!(gUwQ3Q4>&?q2Xhu zm)7xJ8FI`nMZ+Ygk#2jKmvFCY<{VQ6mO&Up#1y`)8dRWJaeK(%g^UXp*IF8h5+Yfd z2Yn`mk)mr#)O|H9fX5YM7u=Yw6p^uPP1Lj@$X$5@5%xxbm*6NtX;$s*20qI7V&kU+ zur_*RXpo5nb!k>?3igWl3O^}9CBGMxAyDic6Mlz=ofWI%S^urVdGJaZg0WS)5k|@V zT`bDI0~qsVl&-{kd8GPK<#}Tp1A>X>>>GGkO~$cpb6P>`>yzqSwa7%uzr9E4rUA2a zU~FTS!%aA-o#>N>I_+wugv3tzVePkw-2DQ*ddvOfm=-#1(uYHVzN`-ZptUyT;$dYX%W?d;jK(yd6}irJFJ# zNx|0B=Cx;|WCE|a({q|G?!e{k59X?=hhcNlettu?cPUKaUi+hDvBJ63@gh{Fd zktJQ@k8cZ2lt05?>6N*PqA5r_ZrGL?js{a|Po%P{2TWm<(7?+7P3C4S(}knd;u}y` zYbhd*octD$YR5k(kv-N1nDqQCBktUrYK%1Y@1l+izQr{NP0nPwxG(s1Id@eZ?iVLa z=)Qx(IriI{7lfQ^1m?KXp;yYu-Wm@Ry79LxA7&RDnmc_HRR6ZWm*uJh4l7loMu#d0 zZyNL~vFDTI#G7c#6$%th$}qA1c$UA#23sdVz{dwao(;x))5VytBzU?PKQeS?HQF4< zWV9FR2gy?^cI`6%>rv0-QDljd=jH`Bp?)7Lu`;rxmibBOmrV zeE`q@DjW>Fv?Y`8=Z*@AWc;5Lp`xlTSU%dqJx!EC@dy&v0tM_Ce zd|ci2%WtOyugAj@9m_gEWQ-JN*iZw3)zQlBTKWsrdB=4$q+s<55CM<=>99~sLTA8w z>2rY;W=!UY+x)eYChZvBeu2CeKowmL4R!$VJ1Mk)6EvH)wg*a?ropRTG$$zV5`>>q zKyF$Ym_OdzvQ(iZ9dO^RA&YYZVoBN)b}vqsEd)VLRmZ((i9X-j5a(bwmUV#jBBEr(q}06&Y@dEKNs^qDcuRP2oEa zGCz(t&g8N8nfa?6-NI2YwY5dVT0{Iv0by&4PE1CyVVEe2y|tko3KgnXZ-UJcU%GpQ zJmcP>;=O?4DX*Fxf0vHu2PB1#QGXfSEu~(V%`N;1|F<7JdQ&T^$k^m=*#6xQ@jWBi z(ySLI$B!t(K{|`z(9v*1CY?e)64HZO)j+;xgRTCs+h%sBWk^>xkSTe5a--U`-fGed z#*Xpf*1SeXbjurHL?w`kUj@8RDl$D;8Ai*{fwYGE3gN03M@xN?y)$cRVGO!wLzlxA zW37ZR>{jVPffNLYs-Xz__qDWFyJWJ_T8F6Y(FbLGKkn@~xVc}Qabnwqw1fFSx&E@( zsld1Rp6^_Y+8rlxP2f`_yec6l1WRI_%zV7|!}e%WKQh_;#&veAXNLtkTmbmO2Xd2U zYbAS~HntLWft=}LL){vVbilQY8Gov!@n$P@P|B+)G{cQmdlp1_39JTs9kC=L2qLZS z{drKyscYgBDn#cOMuqp8x zM+UfGP78~oQ!_^d!A@I0T2MI4=r@~Zztaaf8>blYJ)N-BgsoXrR6Q4CX*IFGPh@Ry zj?*t0t{wnAFso>WX9<76V$Y-A@9*Eb%I}9rKZNfXsRg{7R{$n@R&kPJf=chg%BDpP z-UNwhmAbNRknX%Y9sgwot<;bu+KXXeznvmb?Q8O>?R;(c!YUXgLUv9yVE`MFe1?Ba8uNgOjFjUAyL~+z`vjV~z!sgU^n&Cv$M0)DAbQR0+&r9`Jx=T&+=<@dd zA_58m`XXv-;on|iV1(PCVMvL+O_={Vo{h_V5{f02X1+F=++`W>_q#+kp5-{z zIjFm#L1d}_Q*butQ3IC8$2H>VuSUD)?cde4?W(nXn=I!Xk|Wo*ivCFEqU!4EqfldW zV&mj3zXfQJABQoJ%qT1x*dw?Ueh8<1 zx1wZWO+-`Ei7nFnc^YXc6rrRiBfjgw>bgijiBz8JY_bfwBbVnxM{Vv#ih9*UQosGr zwa8441a;_?$4=p}p*TX7#wH~(pRs!#yW!pMVX*fXIT~n+NyVPQ{9(D&@6GAdz6+tC z-~>@(MkoacsY}qvW57$_dUj#6Yj_k5TNKE7l}@SVR$du7H|ZwHX=-VH7aIO9cuG+> ze5$%wdzDDyE-mBtHy-%3_80OmxsWI339jU~o79ZzjQ2VV)k*7(6W4e3dxUh6s0Jxu zhu3Lx@G;F36aIsxM%6Tm5wJs+sElM_s(Vs4^STx^l7BCihv9;1uq!L6cZDbblc#&3 zpGPb^W-Jn9>Q1xcpiX<$9^^ZJr+HHr@Pha5bCHDEVSk>Q7> zYkv2h%2I3da_VwR=8L{a!lm2e*x{FeKCgqLL!72OOvbmEovUrj0hi^fcf%#l z4-Ssk3O$R$4)0d2wN|NUef7Cs*{}IXdFX1F4?b!7JGeHGC0OqMFmRsSLoM2oon#11 zdgVaJ_%xP3cB!NF+klh|W;+@ET^kz6q&g?&Rvnp^4kGnXJg?)5=U zZB=eiHd5?h7jb60s>nXo?BSm6r%J8z_cM4ao5Uu#qKh#?)5F9FKN!&m>U$1svUt=v; zu=y__W<4uRl|m`;5y29^05UUG=p4tjatxM~tahYz)=L zEi{-gLfZQQ&2LxjE0w0ooT3_bm!eQ3BO_#*F6jb{xciLtmRb8uSMB5am7}(iiKo_3 zTk&^yFyb%9X!7*t;nBr^H&UaqT}@S`AL_Z!EOR6us+Q~YzN&oD@jtcItvT!7P4>Jz zJ>I^&+UGy=OH-@2Cwm;{^iO%bTF^PbNc2ZK?+7mOJ&eN2SP!n2+3#I7LH}g6B>Yz<_?ll{IK_MPgYuYJiJ~#$ z!WPfW`1l|KaFCiaJPB)+_d2bMVQTWgM+8Hw(FjAMTHSwhIEO+=$l&-(tDw57mbURm z@YqB>Su{e!=f4g1V;PkGUTkE(**9wvb&T&%rk~t~R+2Pl`@Qg|MhgoPCRw7bjEd=o zKIYsBNVm*=?~}Xx!gEL8{OW`rWd5X(nBkX^q{bstA3YnZ(6V*ab`z2j6CRtD%C8Y~ncdf~9?0Hg= zL#ubEC6Q1&-&h-y1dTKGX_!I>kvr}lNS$QriJuZN^zC8AL(M`};;KidSeUNd(>f3G z(U5I{k*o)V*uvVJkIP@{OJjE;$xUWb)6&X5N4jhBBO%Q$iZ(E=)){^{Yu*0@60j5p zAT;H8#{BO!o)r2y($A~n#ftx>jC+v5w65<$Y%xBtnN`8(APq+ib@9zh$gl4{Wdz6> zO?aLhZf$F)pwhk|Q@{{;KeF9(|-a&~dz(p8V=aR%d%eMbo(C({4P z)$#NZu2g^yHDrj03y<1Ds6KV5v~;i-!500y%!3?B_|1yZh!B% zRgSmZ1dYX+mPBgT*-t{EFEDY0+`f<5$qQI)z6-ZKXlQ5wiHYb*GFr(+jjTBn8DF-6 zEqkz7CSzWtp-OcnPv}F_BzdEN^TRf5EU}nDo*4U zk*l54n!Vd?6Uu!T{Aqgc*+#C5o4X?8QJJliZ3n9!f~s%a+dG%* zN(}F(9%!n+dN5dMr0YLpQTQ=x(AfTwhxX==o74J}Wc#^-Kw{|8)c%NdT_F;!(Z|q5 z2Wf?nDoFl@rn4!%32sVlT`U2nM3alhnH|pjtbm2nI-#iA*_4w*{ip_0vj9FRr}OZ^ zor~l9I+^6-FM-A0asWovnyqj=ZjXwXF9iQ|Ww)G)rf~5g9~h!UcJX@5c8#LOb{5h| z$(~hsK=H)H`7$}m3~+M9;RJJC>Kpwc`D<4@w`IodjV@LXw*eDFlA_D=nTjQX(RsoC zeaK(a+}P(inS=uKa}x)uH4=HDnu%0jg!YdsvGY63{jIN}D~@ya2WITFVH2^aIYIcE zi1RS^tMJ#+bNHi$tBtHN4aXhGZm|vwm>cu9VsS}$;lqKX^D&N9v!#3b4o`?LxoNS7 zC4zB@;xBqAc~7dhtVO{Vq`B`3s=u|XtFwkD)j1J0hAx`^im9+4rdCR7j<%4h|77b; zn_nl$LDnDv6RO8L!vD^SMO|rzD}rpUi-S4{Dcsy~kEp4wS##_7Su8D34M$bkL-rOmD3?A~7{Hw_aC&1{>%oibda^*Q7j&u*mtigcJ{@Fq_hs!(_%jFM^<*LWxr2<`t{r)DZKlni$q~cxYipk2h^fRM3Hb|4`&HO_w@? z42PmO0phQqWNlh=4(^2%)f*H`7WVC8_&_~SN8fqXkj}*gm1(WS{B>{e-M7&CM z{`{U6>FBASe^;0i!fTH?p54|mjz7oXUe6|T(P2)Ix3qK)?)aQ=ugjW;A-6a|ea2?U%1eY?8Zspcp9b{p z=^9p0P!JitX)`^$#k}|_#e|J&fgYT&*DCJV`J-`i)$I*-yBvibA5y=467Lhk8N7 z`%F6@{6W!ndB}eaFn=tb>tf5FLjg+S!<9Y^2lw=cESFBM4dcXTuQPsSc35E+l>W-6 z2&-}I+cAI0UTZa-`qab|rQUMEVfmNkJir?W-2Oi}c6MF~F3!Guv%rGNCIKe^N7^D% zaA{DPe$9ZQp9cAC49>Ab!1#ar9{LV-N*6LZ*VB<4jAkdxb!uIp?yG-MjfM2o$7ZFT zjQ3#&ZAEGoA%NVvgA_7UusOeIAw$)|=pPyiW2|gsKVKq8n4|zY5!W%WRU7qeez&px zf#6-z{t;S*+0~-SQ|qN_g5JJ9GD$ARuC_O8QYevcxNm0dA>$Rm|M%>7lDH8(3A|V{ zefZieXH3HHHhf};&xSIkQ+=(E?M=%s%d{`CE^z)=$jCn-rv>n#H^lO?P-lxm ze^!EA-^|cUH$b~2Hnb;=nxIb&LeV>QeM?fhl+oC5;Gaupvy{eYR2(oXbmJo<(tP`P z*dzG^+dM$y>VS3B9ldPyJ)c!!#Dt@2*vGorM+u z_GK+9KY@X(2x*xA3?0b-9}1-u?a`U#3^qT9WSa$g1QzPp_z;34j6qhIOP#6y18W;tLkeo1D7DY;X@7Z=oX%;+FL0&m^M@7oNl$oT+2?+wIpM z2Jit04+;42k(!-=%SoQZtg-*G{@wxW-`NoFg>xb~%$pb#rjHwAmNpw;*xg%uXuJqHJ73vRDoC(BG$Yt4w@?+W<% zbUEn1|7Jj+={C_@B$Z+Sq_GWWZ1<=B{(fOE;FGX;$OAEUby!hMy)KKe-R9Qz$=@+= z&8>=RUgh|q3;xB_-N=JyZ$aL(Sn~4pw?Hn{!!iF2DL1kh?HRNl7Vx9#kwgl;gg|47O6<^|AfcqM)v+ZdO;i=jf$< zev@Anr@W9XMW_117L+r#92RFVS}xKS78fVu5crlkkGu%>U_rhoNFkOT@;$21YUsZ~ zA~Kc5R5t85jqV%JIe)g;Z$FpUB%LB?VsUN+{g5;I%K~ravhc%pT?eC#E-s0#yZn64 zpd0*zm?Z6yAA#(UXmg>>bF8 zt6cwe+fU)gz@*LW>+MYxe?BOPsr9(EV3Xuoas)5FB&)+GYx^5x*}=L!-WEd7&0`8> zIDcxefYgmnOa2t(hk?d@^R;F|n*;0%@9pjF;X=ChvMG?&l1k184-9oNBL-dSrLwdS z>yZqJydvKIh^c4O+lcPG(E<;Pny5!maLRUj3yVP~5>y}PS`!R~)tJL;U?`o>iPSGl zn|(EsqG``hgN>kn9seA6eec06D$%fzI`H{#Uy4X|4NKD913Oq}yT0(c!T`$$ z`{T_aiKm38K5gOX5iuA<4-P3h$RHxWp~HO0(xvTPwl1TS$EeG`)L1fVaUgSXy*60< z6gXmSG-g&uaF?r>*`3JIK0UB-B&!v}6mtq>&*@5a7SJ_a&@XtDts5QWw*oudm%kz= zzsdj>_%!JioidZWu@%O8Ec{W{4Px*WaJky1m6-8CLl+Y5 zylyU=3c_q=fuV1&FM71k4GB};(ZE?JE-CBQ^+o4@7rdbQCgJ4!rs|8t?jDvrZ~JI5 z_|d+mYqoNRFDa{92Nn(W9p&F;GTb5`i2bzZIvCiy$M8kxkV;rP@S%-1&m8#UX~Cm_ z@gGIK2bPnI5sDlj5%bC?55kc}DCPwX4(5p((Desh{tgR`vQOH|r$xmNhDl*q z$txzA^$vEliraYz!%R=W7O0sO{vIuC1oJNEzjcFt|E}eiIVVoh_Wr&f#LRzSl_w&hsW-o#X91TUr@ z7Bn+DI5qx3)1r+u!4)vK6oE~>HEqg&NqmM}`I{PKaamav?v4YqjZxNK4I?p!0O-K@ z5Vy&VvFS#2U!EUZ&}ICn5zm^>{a;eqeNmDL3l-7P6~*ra@wFp?a)hfX8|T*;%NF^6 zReg0-lwY*A3>^|fD&1ZFhEC~F6a*PSLO|(~5NQ!mx=XqhkOmoIq#01UOS+^%y1p~s z_1(2Df6ZdOGw+Fg_I~!WS;!`DFB_w8MueYOev~?k&v!XWd z7`rRH(F|i+_kMJ{Nzo%U9BP-@R$snBjxS%A8=FdQ*DV|B{x?*M*=kC6b`pVac-Q9+ z;||+5jYu8yn5xX{1Ldn6UqzVRTh2_webz)(Bh0wLTGs`wS7v7@nd!DK>p~g{HPJ*7 zu)JQpl=Jo4-fT6rw!wR^$!z8(^LBeuduP8bu(FEkOTAYQT+AOkBswF+f_}H%_nNiB zvO_*1qpEf4*WbjHy0ia$ToOp=%?3oG1iI}spPMU6G7gqsd??SmO|d#K*7hbs*a>~! z_NDsf_&N00zQcZsY@)UdH;6OGplEBTS&_i`>{-)6QNaQWe^2Y#$+*f{I1eN0vY4?! zkpFoS%2d`i*v>>tF>lLJetlY1_%d_N)2Fel)>-prTtWuGJSPS0`Spe1M2gyU33ecfXR9$24uE>Va-2p zuF@W!jWzRTCdhklce8IL?Os%EMm?m z{k_K_Xm*rlH<;Ul_xH?(F7uuTf&Z)oeQo94k=^2OF*?R&6+Mmuev+J#Pfh`mzAh*0 z<@-}TPfJr^l=>y%mz8U(tNKN&UZZI?@gu_oZ0zidAj?$YXiZBMDeE*HrQROgf(;m! z1Bls54H^_=S7Ai}4o2L4H8&sOQFQiG(icbn?Ote)2H)!iqHELT9L~X{x z1;wOI+}l~I@yQWltW)6SPi#NE!VN!hYpHnXn+WIYUo*C9FdL75H`!oCt*b_iyOu5; zgn3?eX|qmwNSmyr(mj9cRGRUI)Y#})q0R2LGp)yMU{MbqwI@`#m1rIFl(vVv=Zp88 z(PWbi2I{{^9I>eG2z7kVTyHy=!!*P~7UES?uBdO%RfzBxu=!b=Ca@_H(S&;fe4{Wl zA0xYQ9#Oaz1B2te2nvAH2tw2Z2r++I5j;e6(a1$R}=b5p0 z@``XzsQF#nC4TaHE51e_?}`DVQs{<(9>MVs=PN;eCh@leWD=8{T%T?elT%YICXpLO5a(i;kOyfQFa=XlxgyG$ya?}1_U1`~3N~ok5yNwslZH5H#ToI#u z32n%78LmlpN6GMh`vk02d(3JV;n&4w^ZVpLXYGsRd(^E$%XfSL#&!;3$MG?k&E7zo zFxBRfzM4s++_!XMCdd%#JDhi3UJD=Zymg*Qy(A;oDx3W*BFw&R(sVjBFZA?aGw^in zkxH_$1y@+=i52pV(C^=>9Nqh$Q3D>3CcmMgluLU9=+4_O+=t`WR{Ll*N5<7cq?129`4s4Cff6PCfJacc!O5k@DgMK5=Xzp0TF ze<1cPQzW>D(ZhJRgq0}AlBp)+;-203b>Oebq>Y(Ms;Cv!oQL0NluCTk!(2Su_2Jjm z*kZq>?jXJ!?yuC+9`svekXqeRZjIRH*!{QO8(u-*O47(W*sRBb-_v|{F+uUSu_~Ww z#mO|Py$>f_0(Uw+-|PFXii#hLfYS_)gJAd=C1g{o;-Y8{Rny5_@K~(IPEBmkE2>E) zFO{FlOK`KM1_rKn#{rch%C@$}+w8`Yhlf1_Xjom1k+_S7)xHq0&-C2Jsa!V5WDZ$L zW!AnELJxg78%MZ}<_{*rxtOG00i%E+Vm6A<_YTpb?bn;+70x%T)~HXz985LW3{3}F ziJlw1u2#^@M*rH_B3Dxz!dmqKH$Evr2N6_BFzsPtQZDtj=xN)WSHx6Xz2CJL`uYGh zHMOTwNVSFVQ-7QS>`Zj*^XO555?JgY`5u#D6^d5|^nxM9y0Eww7WXbeEvnj2qDGq= zP0+-yt!;=$zrW?nBCKDjV=3otL`|?GSGxR-)U&SZgO*Z+CsLm!RE{dpN3O9o!-!^B zWxVD%YlOc)aP)lY-Q(KOx4t>Y6cXu$zdVK@OT8H;&(n-2dg-rRh1EBJW>s0KY>dXsyXr3<* zleG^DoYW?)nwivj1GsgLHAURoPDV{SKFzw&?sP3-Ofon{3Ca@t9ab*IBe_0baOjnUj`e**}RQ&1mHsitMWfcQ3S$08gx5f_p&VQhY zDP2__#47ga20QGGd+ElGgnIDhSb#`H<2-8sBtpO*87R+WN8gg6q;kmM)wEFeo3->2mm~86*KS&ey>~*0(=k{G2*7!aLe+E5E z-^fJqkph7BzAKWJRaEqOZoZ5PoZwId2!gMJQoam$TS{gC75OwlfmHqnmS4z^Rhzgd zMSADxFd&pqMRX!a_wmtjve1_?D(J%-jRj{q)Kne3uwcZjed0yT`h%L1Qa!;f+m8(L zWuAow)4HOd1pKP5+9mXQ}Z};{iw^f?*sW zC09xEX`&8hwTA2((*r8-CsJlxOCfh@TZZ-lJ@`CH#l&1Sj&xrPV*c{)ce2DA^e~=b z!PO$@B&ygnri<=#Mt+51$jBXoMvk5!$F0(Kj7N`(Q01R60+>{pnHEy}bPM_br~=>&D5u*j&JQ#*o|}b^uEv ztr@_tmHxL;pxJ$EoJP`(D>pY+qhl%f(5p^a$50PCIC8f>Do-^!7tkLkab+3{I&~BH zC_0sLlJ_vQ^Wo2IMif*J&$2|$bmE<#xHtP~85-UXAz_RH#Kc!zOq16iCtjf;ZY1C` z{b{u~qk)W@FlTqN5Qd_QCk*bhvalqKsXO7LE{TtLB_!^JhK545w-RFy9~F2ty^HJv z5uiMqk-{qCJgY$sZdnb$`$Q8*z%;?jUC9O=UuXVI%F2p<*&gy$v8hc+^%RqGGiGCvgs=MyeUEx`-ci}VZd!HLcbQ7ARgHp?`$ z4UkmszuJVjx$(b#{aXCO;a%xd_PD7cyn0+A1mmTyMS-xw0u;c7CZ&Kkjc70%fNxEh zS2zS3lUZ6>l{hS&b&hu$*4i$H@s>Sxw2@u+i;jU&W4zs}xU58Yv3{e3`~b|=^4?J_ zT6pn*F*YO2Gx-?pq^7R^aHh^dj})u1(89t396k~tTguHbZxIB^AnYL@%^bbcpCJ{S zn#!#`EGwol%=84UkkmixB%#OAA|Eg}z{duM9ac#;dmZcOzhZVe@_$@&nR*(B+(TYg z*iSte|*!ITRdNO^?9KlX2yXslk|kfOV|`2~dywURv6*a)w5oL?4X1 z(wrOr@U)-no|p6DZ6<~Mt>7gEJB6O3E7t4>(*}>7FmP&%2_{AOkEmt3r_@P9)3$L>e+`mk+sW3c=&pibb| zzY0{)+9)BQ5iafZmrK-f_Dj+@knu=ER2n&ME2@lX?FMSynvtCR5yWA>9#b3DSZhBh zW1;GkK@MLW0r8X03QY&*1*pHU0BtrOPt68dljIjQaUjN`b;r9Ghp!3Oijby ztf*0ved7V~>5zzti7SWwN1Eb%?T7h<;7{Xpt^uE706a=0yI3^5v*<9IE2vQR^ zEpBz`ChfZn4!c>A7IR8+aozZw3?V1)if089yUoULD(4eol_nWA{pvlVTWLGbW)yvxsp4h zJAS>J4z%GY!(uqK;PX$ObW^=3^*!4DthwNlUM6+ClmwtdJyV67(|r^Iksu7Xc+#YH$Dv zI$S0zEj|nbg7`~0;M+5mx0`Ljgq7Z>j=*v(fGQyQELq=#u_5c1B}j%i4837*^b434{ESdY7*?JGVfZ|e!mpb&8}Lf6UC?E1aK--cg(n056lGl;_MDMdIw z(t8l%i|$kTKD2k%4K?ma|C>VNbkJetM|uYGun$0!)33OYI=;gE`~?syLRZIhf>0|; zC*%n_IPt8VITDSdeqq7WH||7L&Bm%saGX|`N;B-qHI^CtoKhPeYLeP&uoC(cOI>NU zOrPRwExT(&qq221Y3ABl4_EWS#y+5X*zj?$Kz7O)WXs;SQ)DM`jW+AfajtPRNb5+) zq&jh)34Mr%eYr1_(6h+I+^sO+-mkUlqF1wim--3gq-4Rt^{6yoC}YD$LvfUE7hUl-fcQzE0W zGW#By55-1iQOgGYkh;kJ-9MG`o&Bz|eXK=rBJ!^3Kvp=3oasl{$x2*>YhAo-)>P!! zPuY%HgsHimB)wgwrx)hU+uyGxDMl_=E9j=nyHeIQ#k#(Gu3I%cSdq_;B#)|0%@mOvB zJdD$0IsB*9a@{>uE>nz~cXJMB6@4X>{weFU46Rr1z4z_+h9))jmz{2StNcBaWceTV zm)oSo5v?e90b=_+1MJ-K{SWK5fn%ua^ z^BrT~+Cv`5dQtv|EDQ~o=GMkuRmwSM-0k>Jv$5@7KV%#JgIZIINOk?Q8zP0jJ|t{4 z55NEhcBY?yJSmC*m_TomFa&llcH)KZZ$nGg4IiQGWzC9T$S zL;=I}s9j`pxu8OS*49!V{c^=CZzp7|k|EdU$5$9Xsc@Fh?=qU*h@6XiEif4qDvIKK z5J@A5$q5Zt8E_`++xl5_nc*^-D(-5)JBrBiXwA90cTZlS4Zjp_5HQlX#pEytu{Vf) zPbK5JKQobvD*1kYV*;0%LTx!aI~BFTVLj8^d0KSa8T4_S=`h2zMapd^T!D;kv4O)0 z?2BhQ(Cg8xiI9KZudGr4OXj;bSx^^S7n|v>s5RYUUCRQAFwOnxQA+3ZKIO8pz%M~w z^^R(GO7X|B&!gS&Wj@D$al})wv{-0SzoFC8iGB7cRC!42h&?<;wN756Q~9qc-V*tr z?4-}HP@ybYN1y|Pce<>YeEkFE5GL5+9Z^eQpw&%Ttc)bL_d-~69eEh&M<0zbQar-o zr+Dm;$D-JViby#Y=_2u@Oh^7+|T2_pz^NJ$D}s|MQWJnbZ^bH8{UhG^4>R z@Pr@*_rrYas3^ac>Wui+=gmBvZT4Wm$?Rskdo9fH?1vXL{IZ)T-0XJ3=tSS(;rtk4q?oD4pVp z#m`2}=M5K@v;u&kszMsf|IURe$HE&Z^{NhQbO&s$u;y?$TJ%tZKB|E@l_9@;i7SYXX^oE`Me z=7SQlni7qBg_yO9ga$jk4Qtyp*tvu5wX6|ZWyS`*yW%@53eS7uiizHZ+Z@>a4qj{8 z{5XqaLOv`JPnIf`v^YndwBV^jMoiBi#J|VJV)(Z!XgyQ8+;jR{7ei1Cp;-O)>mCEJ zl)cLGSQZE&bq?@3C?FJnUtvVrKgK+R5JgW9y`LMDI2{>8krf%J;@J(ATE!#Iy)INd z&&a121a!m&SAwo3eR;nq)qo-tX&;!+WLuYZMjIF!HOu(GfTIalATfx z3BPp(_09QtgkR3(N)wQRRdmqaC!H-B?LVP|OLq)l>h@HIHna~FrTEy?6x*yhGnn)T z!}+>4#ze=o literal 0 HcmV?d00001 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..77e9b1074 --- /dev/null +++ b/doc/source/development/index.rst @@ -0,0 +1,9 @@ +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/getting_started/index.rst b/doc/source/getting_started/index.rst new file mode 100644 index 000000000..649d04cd1 --- /dev/null +++ b/doc/source/getting_started/index.rst @@ -0,0 +1,9 @@ +Getting Started +=============== + +.. toctree:: + :maxdepth: 1 + + installation + quick_start + legal \ No newline at end of file diff --git a/doc/source/getting_started/installation.rst b/doc/source/getting_started/installation.rst new file mode 100644 index 000000000..ee1659d19 --- /dev/null +++ b/doc/source/getting_started/installation.rst @@ -0,0 +1,17 @@ +******************** +Installation Guide +******************** + +Install `yfinance` using `pip`: + +.. code-block:: bash + + $ pip install yfinance --upgrade --no-cache-dir + +To install with optional dependencies, replace `optional` with: `nospam` for `caching-requests `_, `repair` for `price repair `_, or `nospam`, `repair` for both: + +.. code-block:: bash + + $ pip install "yfinance[optional]" + +For required dependencies, check out the `requirements file <./requirements.txt>`_, and for all dependencies, see the `setup.py file <./setup.py#L62>`_. diff --git a/doc/source/getting_started/legal.rst b/doc/source/getting_started/legal.rst new file mode 100644 index 000000000..e79c958bb --- /dev/null +++ b/doc/source/getting_started/legal.rst @@ -0,0 +1,12 @@ +******************** +Legal Information +******************** + +yfinance is distributed under the Apache Software License. See the `LICENSE.txt <../../../../LICENSE.txt>`_ file 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. + +Refer to Yahoo!'s terms of use: + +- `API Terms `_ +- `Yahoo Terms `_ diff --git a/doc/source/getting_started/quick_start.rst b/doc/source/getting_started/quick_start.rst new file mode 100644 index 000000000..9b2a12e74 --- /dev/null +++ b/doc/source/getting_started/quick_start.rst @@ -0,0 +1,30 @@ +******************** +Quick Start +******************** + +The Ticker module allows you to access ticker data in a more Pythonic way: + +.. code-block:: 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 actions (dividends, splits, capital gains) + msft.actions + msft.dividends + msft.splits + +To work with multiple tickers, use: + +.. code-block:: python + + tickers = yf.Tickers('msft aapl goog') + tickers.tickers['MSFT'].info + tickers.tickers['AAPL'].history(period="1mo") diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 000000000..030791a3d --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1,30 @@ +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. + +.. toctree:: + :maxdepth: 3 + :hidden: + :titlesonly: + + getting_started/index + user_guide/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..8a08c45f0 --- /dev/null +++ b/doc/source/reference/examples/ticker.py @@ -0,0 +1,77 @@ +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 \ No newline at end of file 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..65338866b --- /dev/null +++ b/doc/source/reference/index.rst @@ -0,0 +1,33 @@ +======================= +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.sector_industry + yfinance.functions \ No newline at end of file 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.sector_industry.rst b/doc/source/reference/yfinance.sector_industry.rst new file mode 100644 index 000000000..181376082 --- /dev/null +++ b/doc/source/reference/yfinance.sector_industry.rst @@ -0,0 +1,32 @@ +======================= +Sector and Industry +======================= + +.. currentmodule:: yfinance + +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.ticker_tickers.rst b/doc/source/reference/yfinance.ticker_tickers.rst new file mode 100644 index 000000000..3f66a6246 --- /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 + + +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 \ No newline at end of file diff --git a/doc/source/user_guide/index.rst b/doc/source/user_guide/index.rst new file mode 100644 index 000000000..8e45e8a77 --- /dev/null +++ b/doc/source/user_guide/index.rst @@ -0,0 +1,11 @@ +User Guide +========== + +.. toctree:: + :maxdepth: 1 + + logging + proxy + smart_scraping + persistent_cache + multi_level_columns \ No newline at end of file diff --git a/doc/source/user_guide/logging.rst b/doc/source/user_guide/logging.rst new file mode 100644 index 000000000..3cd358e62 --- /dev/null +++ b/doc/source/user_guide/logging.rst @@ -0,0 +1,11 @@ +Logging in yfinance +=================== + +`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/user_guide/multi_level_columns.rst b/doc/source/user_guide/multi_level_columns.rst new file mode 100644 index 000000000..1714c5dc6 --- /dev/null +++ b/doc/source/user_guide/multi_level_columns.rst @@ -0,0 +1,13 @@ +****************************** +Managing Multi-Level Columns +****************************** + +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/user_guide/persistent_cache.rst b/doc/source/user_guide/persistent_cache.rst new file mode 100644 index 000000000..ed798c4d5 --- /dev/null +++ b/doc/source/user_guide/persistent_cache.rst @@ -0,0 +1,16 @@ +****************************** +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()`: + +.. 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/user_guide/proxy.rst b/doc/source/user_guide/proxy.rst new file mode 100644 index 000000000..69b94e769 --- /dev/null +++ b/doc/source/user_guide/proxy.rst @@ -0,0 +1,11 @@ +********************* +Using a 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/user_guide/smart_scraping.rst b/doc/source/user_guide/smart_scraping.rst new file mode 100644 index 000000000..300b15600 --- /dev/null +++ b/doc/source/user_guide/smart_scraping.rst @@ -0,0 +1,41 @@ +****************************** +Smarter Scraping with Caching +****************************** + + +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"), + ) 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/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 5a0630501..87e108a23 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,56 +39,41 @@ 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 - :Parameters: - tickers : str, list - List of tickers to download - period : str - Valid periods: 1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max - Either Use period parameter or use start and end - interval : str - Valid intervals: 1m,2m,5m,15m,30m,60m,90m,1h,1d,5d,1wk,1mo,3mo - Intraday data cannot extend last 60 days - start: str - Download start date string (YYYY-MM-DD) or _datetime, inclusive. - Default is 99 years ago - E.g. for start="2020-01-01", the first data point will be on "2020-01-01" - end: str - Download end date string (YYYY-MM-DD) or _datetime, exclusive. - Default is now - E.g. for end="2023-01-01", the last data point will be on "2022-12-31" - group_by : str - Group by 'ticker' or 'column' (default) - prepost : bool - Include Pre and Post market data in results? - Default is False - auto_adjust: bool - Adjust all OHLC automatically? Default is False - repair: bool - Detect currency unit 100x mixups and attempt repair - Default is False - keepna: bool - Keep NaN rows returned by Yahoo? - Default is False - actions: bool - Download dividend + stock splits data. Default is False - threads: bool / int - How many threads to use for mass downloading. Default is True - ignore_tz: bool - When combining from different timezones, ignore that part of datetime. - Default depends on interval. Intraday = False. Day+ = True. - proxy: str - Optional. Proxy server URL scheme. Default is None - rounding: bool - Optional. Round values to 2 decimal places? - timeout: None or float - If not None stops waiting for a response after given number of - seconds. (Can also be a fraction of a second e.g. 0.01) - session: None or Session - Optional. Pass your own session object to be used for all requests - multi_level_index: bool - Optional. Always return a MultiIndex DataFrame? Default is False + multi_level_index=True) -> Union[_pd.DataFrame, None]: + """ + Download yahoo tickers + + Args: + tickers (str or list): List of tickers to download. + period (str): Time period to download. + Valid periods are: '1d', '5d', '1mo', '3mo', '6mo', '1y', '2y', '5y', '10y', 'ytd', 'max'. + Either use `period` or specify `start` and `end`. + interval (str): Data interval. + Valid intervals are: '1m', '2m', '5m', '15m', '30m', '60m', '90m', '1h', '1d', '5d', '1wk', '1mo', '3mo'. + Intraday data is limited to the last 60 days. + start (str): Start date (YYYY-MM-DD) or _datetime, inclusive. + Default is 99 years ago. + Example: For `start="2020-01-01"`, the first data point will be "2020-01-01". + end (str): End date (YYYY-MM-DD) or _datetime, exclusive. + Default is the current date. + Example: For `end="2023-01-01"`, the last data point will be "2022-12-31". + group_by (str): Group data by 'ticker' or 'column'. Default is 'column'. + prepost (bool): Include pre and post market data in results? Default is False. + auto_adjust (bool): Automatically adjust all OHLC data? Default is False. + repair (bool): Detect and repair currency unit mixups (e.g., 100x errors)? Default is False. + keepna (bool): Keep rows with NaN values returned by Yahoo? Default is False. + actions (bool): Download dividend and stock split data? Default is False. + threads (bool or int): Number of threads for mass downloading. Default is True (automatically determines the number of threads). + ignore_tz (bool): Ignore timezones when combining data across timezones? + Default depends on the interval. For intraday intervals, the default is False. For daily and above, the default is True. + proxy (str, optional): URL of the proxy server. Default is None. + rounding (bool, optional): Round values to two decimal places? Default is False. + timeout (None or float, optional): Maximum time to wait for a response, in seconds. Can be a fraction of a second (e.g., 0.01). Default is None. + session (None or Session, optional): Pass a custom session object for all requests. Default is None. + multi_level_index (bool): Optional. Always return a MultiIndex DataFrame? Default is False + + Returns: + pd.DataFrame or None """ logger = utils.get_yf_logger() 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/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 0968f9d15..e300ae65d 100644 --- a/yfinance/utils.py +++ b/yfinance/utils.py @@ -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 From 5d8b444281ba63c8aaaef09069a74e37a90cf714 Mon Sep 17 00:00:00 2001 From: ValueRaider Date: Tue, 5 Nov 2024 22:05:45 +0000 Subject: [PATCH 2/4] Action deploy_doc: enable manual trigger --- .github/workflows/deploy_doc.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy_doc.yml b/.github/workflows/deploy_doc.yml index 400a91492..3afaa75f2 100644 --- a/.github/workflows/deploy_doc.yml +++ b/.github/workflows/deploy_doc.yml @@ -4,6 +4,7 @@ on: push: branches: - dev-documented + workflow_dispatch: jobs: build: @@ -39,4 +40,4 @@ jobs: publish_branch: documentation publish_dir: doc/_build/html destination_dir: docs - enable_jekyll: false \ No newline at end of file + enable_jekyll: false From ce8f8a0b305e6edc91c1f1594ccf5acb70829a8b Mon Sep 17 00:00:00 2001 From: ValueRaider Date: Sat, 9 Nov 2024 11:34:57 +0000 Subject: [PATCH 3/4] Reorganise documentation Add minimal quick-start to root Simplify examples/ticker.py Separate Ticker API into more groups in reference/index.rst Fix some duplicate labels --- doc/source/_templates/autosummary/class.rst | 21 +++-- .../caching.rst} | 24 +++++- doc/source/advanced/index.rst | 11 +++ .../{user_guide => advanced}/logging.rst | 4 +- .../multi_level_columns.rst | 6 +- doc/source/{user_guide => advanced}/proxy.rst | 6 +- doc/source/development/index.rst | 3 +- doc/source/getting_started/index.rst | 9 --- doc/source/getting_started/installation.rst | 17 ---- doc/source/getting_started/legal.rst | 12 --- doc/source/getting_started/quick_start.rst | 30 ------- doc/source/index.rst | 56 +++++++++++-- doc/source/reference/examples/ticker.py | 81 +++---------------- doc/source/reference/index.rst | 14 +++- doc/source/reference/yfinance.analysis.rst | 81 +++++++++++++++++++ doc/source/reference/yfinance.financials.rst | 29 +++++++ doc/source/reference/yfinance.funds_data.rst | 11 +++ .../reference/yfinance.price_history.rst | 9 +++ .../reference/yfinance.sector_industry.rst | 6 +- doc/source/reference/yfinance.stock.rst | 50 ++++++++++++ .../reference/yfinance.ticker_tickers.rst | 6 +- doc/source/user_guide/index.rst | 11 --- doc/source/user_guide/persistent_cache.rst | 16 ---- 23 files changed, 311 insertions(+), 202 deletions(-) rename doc/source/{user_guide/smart_scraping.rst => advanced/caching.rst} (68%) create mode 100644 doc/source/advanced/index.rst rename doc/source/{user_guide => advanced}/logging.rst (85%) rename doc/source/{user_guide => advanced}/multi_level_columns.rst (86%) rename doc/source/{user_guide => advanced}/proxy.rst (67%) delete mode 100644 doc/source/getting_started/index.rst delete mode 100644 doc/source/getting_started/installation.rst delete mode 100644 doc/source/getting_started/legal.rst delete mode 100644 doc/source/getting_started/quick_start.rst create mode 100644 doc/source/reference/yfinance.analysis.rst create mode 100644 doc/source/reference/yfinance.financials.rst create mode 100644 doc/source/reference/yfinance.funds_data.rst create mode 100644 doc/source/reference/yfinance.price_history.rst create mode 100644 doc/source/reference/yfinance.stock.rst delete mode 100644 doc/source/user_guide/index.rst delete mode 100644 doc/source/user_guide/persistent_cache.rst diff --git a/doc/source/_templates/autosummary/class.rst b/doc/source/_templates/autosummary/class.rst index a2fbdb96f..12b03d75d 100644 --- a/doc/source/_templates/autosummary/class.rst +++ b/doc/source/_templates/autosummary/class.rst @@ -1,4 +1,6 @@ -{{ fullname | escape | underline}} +:orphan: + +{{ objname | escape | underline }} .. currentmodule:: {{ module }} @@ -7,24 +9,21 @@ {% block attributes %} {% if attributes %} .. rubric:: Attributes - - .. autosummary:: - :toctree: attributes + {% for item in attributes %} - ~{{ name }}.{{ item }} + .. autoattribute:: {{ item }} + :noindex: {%- endfor %} {% endif %} - {% endblock %} - + {% endblock attributes %} {% block methods %} {% if methods %} .. rubric:: Methods - .. autosummary:: - :toctree: methods {% for item in methods %} - ~{{ name }}.{{ item }} + .. automethod:: {{ item }} + :noindex: {%- endfor %} {% endif %} - {% endblock %} \ No newline at end of file + {% endblock methods %} diff --git a/doc/source/user_guide/smart_scraping.rst b/doc/source/advanced/caching.rst similarity index 68% rename from doc/source/user_guide/smart_scraping.rst rename to doc/source/advanced/caching.rst index 300b15600..744377241 100644 --- a/doc/source/user_guide/smart_scraping.rst +++ b/doc/source/advanced/caching.rst @@ -1,7 +1,8 @@ -****************************** -Smarter Scraping with Caching -****************************** +Caching +======= +Smarter Scraping +---------------- Install the `nospam` package to cache API calls and reduce spam to Yahoo: @@ -39,3 +40,20 @@ Combine `requests_cache` with rate-limiting to avoid triggering Yahoo's rate-lim 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/user_guide/logging.rst b/doc/source/advanced/logging.rst similarity index 85% rename from doc/source/user_guide/logging.rst rename to doc/source/advanced/logging.rst index 3cd358e62..89464871d 100644 --- a/doc/source/user_guide/logging.rst +++ b/doc/source/advanced/logging.rst @@ -1,5 +1,5 @@ -Logging in yfinance -=================== +Logging +======= `yfinance` uses the `logging` module to handle messages. By default, only errors are logged. diff --git a/doc/source/user_guide/multi_level_columns.rst b/doc/source/advanced/multi_level_columns.rst similarity index 86% rename from doc/source/user_guide/multi_level_columns.rst rename to doc/source/advanced/multi_level_columns.rst index 1714c5dc6..434396aea 100644 --- a/doc/source/user_guide/multi_level_columns.rst +++ b/doc/source/advanced/multi_level_columns.rst @@ -1,6 +1,6 @@ -****************************** -Managing Multi-Level Columns -****************************** +************************ +Multi-Level Column Index +************************ The following answer on Stack Overflow is for `How to deal with multi-level column names downloaded with yfinance? `_ diff --git a/doc/source/user_guide/proxy.rst b/doc/source/advanced/proxy.rst similarity index 67% rename from doc/source/user_guide/proxy.rst rename to doc/source/advanced/proxy.rst index 69b94e769..a1868a3bb 100644 --- a/doc/source/user_guide/proxy.rst +++ b/doc/source/advanced/proxy.rst @@ -1,6 +1,6 @@ -********************* -Using a Proxy Server -********************* +************ +Proxy Server +************ You can download data via a proxy: diff --git a/doc/source/development/index.rst b/doc/source/development/index.rst index 77e9b1074..d6eb1d984 100644 --- a/doc/source/development/index.rst +++ b/doc/source/development/index.rst @@ -1,5 +1,6 @@ +=========== Development -=============================== +=========== .. toctree:: :maxdepth: 1 diff --git a/doc/source/getting_started/index.rst b/doc/source/getting_started/index.rst deleted file mode 100644 index 649d04cd1..000000000 --- a/doc/source/getting_started/index.rst +++ /dev/null @@ -1,9 +0,0 @@ -Getting Started -=============== - -.. toctree:: - :maxdepth: 1 - - installation - quick_start - legal \ No newline at end of file diff --git a/doc/source/getting_started/installation.rst b/doc/source/getting_started/installation.rst deleted file mode 100644 index ee1659d19..000000000 --- a/doc/source/getting_started/installation.rst +++ /dev/null @@ -1,17 +0,0 @@ -******************** -Installation Guide -******************** - -Install `yfinance` using `pip`: - -.. code-block:: bash - - $ pip install yfinance --upgrade --no-cache-dir - -To install with optional dependencies, replace `optional` with: `nospam` for `caching-requests `_, `repair` for `price repair `_, or `nospam`, `repair` for both: - -.. code-block:: bash - - $ pip install "yfinance[optional]" - -For required dependencies, check out the `requirements file <./requirements.txt>`_, and for all dependencies, see the `setup.py file <./setup.py#L62>`_. diff --git a/doc/source/getting_started/legal.rst b/doc/source/getting_started/legal.rst deleted file mode 100644 index e79c958bb..000000000 --- a/doc/source/getting_started/legal.rst +++ /dev/null @@ -1,12 +0,0 @@ -******************** -Legal Information -******************** - -yfinance is distributed under the Apache Software License. See the `LICENSE.txt <../../../../LICENSE.txt>`_ file 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. - -Refer to Yahoo!'s terms of use: - -- `API Terms `_ -- `Yahoo Terms `_ diff --git a/doc/source/getting_started/quick_start.rst b/doc/source/getting_started/quick_start.rst deleted file mode 100644 index 9b2a12e74..000000000 --- a/doc/source/getting_started/quick_start.rst +++ /dev/null @@ -1,30 +0,0 @@ -******************** -Quick Start -******************** - -The Ticker module allows you to access ticker data in a more Pythonic way: - -.. code-block:: 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 actions (dividends, splits, capital gains) - msft.actions - msft.dividends - msft.splits - -To work with multiple tickers, use: - -.. code-block:: python - - tickers = yf.Tickers('msft aapl goog') - tickers.tickers['MSFT'].info - tickers.tickers['AAPL'].history(period="1mo") diff --git a/doc/source/index.rst b/doc/source/index.rst index 030791a3d..0f7a94722 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -1,8 +1,8 @@ yfinance documentation -============================================== +====================== Download Market Data from Yahoo! Finance's API ------------------------------------------------- +---------------------------------------------- .. admonition:: IMPORTANT LEGAL DISCLAIMER @@ -19,12 +19,56 @@ Download Market Data from Yahoo! Finance's API 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: 3 - :hidden: + :maxdepth: 1 :titlesonly: - getting_started/index - user_guide/index + advanced/index reference/index development/index diff --git a/doc/source/reference/examples/ticker.py b/doc/source/reference/examples/ticker.py index 8a08c45f0..00a4f7c06 100644 --- a/doc/source/reference/examples/ticker.py +++ b/doc/source/reference/examples/ticker.py @@ -1,77 +1,22 @@ import yfinance as yf -msft = yf.Ticker("MSFT") - -# get all stock info -msft.info +dat = yf.Ticker("MSFT") # 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 +dat.history(period='1mo') -# 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 +# options +dat.option_chain(dat.options[0]).calls -# show ISIN code - *experimental* -# ISIN = International Securities Identification Number -msft.isin +# get financials +dat.balance_sheet +dat.quarterly_income_stmt -# show options expirations -msft.options +# dates +dat.calendar -# show news -msft.news +# general info +dat.info -# get option chain for specific expiration -opt = msft.option_chain('YYYY-MM-DD') -# data available via: opt.calls, opt.puts \ No newline at end of file +# analysis +dat.analyst_price_targets diff --git a/doc/source/reference/index.rst b/doc/source/reference/index.rst index 65338866b..f1807da27 100644 --- a/doc/source/reference/index.rst +++ b/doc/source/reference/index.rst @@ -1,6 +1,6 @@ -======================= +============= API Reference -======================= +============= Overview -------- @@ -27,7 +27,13 @@ The following are the publicly available classes, and functions exposed by the ` :maxdepth: 1 :hidden: - + yfinance.ticker_tickers + yfinance.stock + yfinance.financials + yfinance.analysis yfinance.sector_industry - yfinance.functions \ No newline at end of file + 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.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 index 181376082..14f2f1f2b 100644 --- a/doc/source/reference/yfinance.sector_industry.rst +++ b/doc/source/reference/yfinance.sector_industry.rst @@ -4,8 +4,8 @@ Sector and Industry .. currentmodule:: yfinance -Class ------------- +Sector class +-------------- The `Sector` and `Industry` modules provide access to the Sector and Industry information. .. autosummary:: @@ -20,7 +20,7 @@ The `Sector` and `Industry` modules provide access to the Sector and Industry in Map of sector and industry Sample Code ------------- +--------------------- To initialize, use the relevant sector or industry key as below. .. literalinclude:: examples/sector_industry.py 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 index 3f66a6246..a3ff602e2 100644 --- a/doc/source/reference/yfinance.ticker_tickers.rst +++ b/doc/source/reference/yfinance.ticker_tickers.rst @@ -16,8 +16,8 @@ The `Ticker` module, allows you to access ticker data in a Pythonic way. Tickers -Sample Code ------------- +Ticker Sample Code +------------------ The `Ticker` module, allows you to access ticker data in a Pythonic way. .. literalinclude:: examples/ticker.py @@ -43,4 +43,4 @@ If you want to use a proxy server for downloading data, use: To initialize multiple `Ticker` objects, use `Tickers` module .. literalinclude:: examples/tickers.py - :language: python \ No newline at end of file + :language: python diff --git a/doc/source/user_guide/index.rst b/doc/source/user_guide/index.rst deleted file mode 100644 index 8e45e8a77..000000000 --- a/doc/source/user_guide/index.rst +++ /dev/null @@ -1,11 +0,0 @@ -User Guide -========== - -.. toctree:: - :maxdepth: 1 - - logging - proxy - smart_scraping - persistent_cache - multi_level_columns \ No newline at end of file diff --git a/doc/source/user_guide/persistent_cache.rst b/doc/source/user_guide/persistent_cache.rst deleted file mode 100644 index ed798c4d5..000000000 --- a/doc/source/user_guide/persistent_cache.rst +++ /dev/null @@ -1,16 +0,0 @@ -****************************** -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()`: - -.. code-block:: python - - import yfinance as yf - yf.set_tz_cache_location("custom/cache/location") \ No newline at end of file From fffe89e691a2833534213ba86dba0e8db71958cc Mon Sep 17 00:00:00 2001 From: ValueRaider Date: Sun, 17 Nov 2024 11:49:05 +0000 Subject: [PATCH 4/4] README: simplify a little --- README.md | 40 ++++++++++++++-------------------------- 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 2c22fdd9a..bd47e8f39 100644 --- a/README.md +++ b/README.md @@ -32,43 +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 -## Main Features -- `Ticker` module: Class for accessing single ticker data. -- `Tickers` module: Class for handling multiple tickers. -- `download` Efficiently download market data for multiple tickers. -- `Sector` and `Industry` modules : Classes for accessing sector and industry information. -- Market Screening: `EquityQuery` and `Screener` to build query and screen the market. -- Caching and Smart Scraping +- `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 -## Documentation -The official documentation is available on [ranaroussi.github.io/yfinance](https://ranaroussi.github.io/yfinance/index.html) +## **NEW DOCUMENTATION WEBSITE**: [ranaroussi.github.io/yfinance](https://ranaroussi.github.io/yfinance/index.html) ## Installation -Install `yfinance` using `pip`: +Install `yfinance` from PYPI using `pip`: ``` {.sourceCode .bash} -$ pip install yfinance --upgrade --no-cache-dir +$ pip install yfinance ``` -[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: - -``` {.sourceCode .bash} -$ pip install "yfinance[optional]" -``` - -[Required dependencies](./requirements.txt) , [all dependencies](./setup.py#L62). - -The list of changes can be found in the [changelog](https://github.com/ranaroussi/yfinance/blob/main/CHANGELOG.rst) - +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 --- @@ -77,7 +65,6 @@ The list of changes can be found in the [changelog](https://github.com/ranarouss **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 @@ -93,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** +