Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/dev' into feature/calendar
Browse files Browse the repository at this point in the history
# Conflicts:
#	yfinance/scrapers/quote.py
  • Loading branch information
ghofi-dev committed Dec 14, 2023
2 parents 6116fdf + a679060 commit 2f143c0
Show file tree
Hide file tree
Showing 8 changed files with 377 additions and 99 deletions.
67 changes: 28 additions & 39 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,26 @@ Yahoo! finance API is intended for personal use only.**

---

## Installation

Install `yfinance` using `pip`:

``` {.sourceCode .bash}
$ pip install yfinance --upgrade --no-cache-dir
```

[With Conda](https://anaconda.org/ranaroussi/yfinance).

To install with optional dependencies, replace `optional` with: `nospam` for [caching-requests](#smarter-scraping), `repair` for [price repair](https://github.com/ranaroussi/yfinance/wiki/Price-repair), or `nospam,repair` for both:

``` {.sourceCode .bash}
$ pip install yfinance[optional]
```

[Required dependencies](./requirements.txt) , [all dependencies](./setup.py#L62).

---

## Quick Start

### The Ticker module
Expand Down Expand Up @@ -87,6 +107,9 @@ msft.quarterly_cashflow
msft.major_holders
msft.institutional_holders
msft.mutualfund_holders
msft.insider_transactions
msft.insider_purchases
msft.insider_roster_holders

# 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.
Expand Down Expand Up @@ -155,9 +178,10 @@ data = yf.download("SPY AAPL", period="1mo")

### Smarter scraping

To use a custom `requests` session (for example to cache calls to the
API or customize the `User-agent` header), pass a `session=` argument to
the Ticker constructor.
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
Expand All @@ -168,7 +192,7 @@ ticker = yf.Ticker('msft', session=session)
ticker.actions
```

Combine a `requests_cache` with rate-limiting to avoid triggering Yahoo's rate-limiter/blocker that can corrupt data.
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
Expand Down Expand Up @@ -230,41 +254,6 @@ yf.set_tz_cache_location("custom/cache/location")

---

## Installation

Install `yfinance` using `pip`:

``` {.sourceCode .bash}
$ pip install yfinance --upgrade --no-cache-dir
```

Test new features by installing betas, provide feedback in [corresponding Discussion](https://github.com/ranaroussi/yfinance/discussions):
``` {.sourceCode .bash}
$ pip install yfinance --upgrade --no-cache-dir --pre
```

To install `yfinance` using `conda`, see
[this](https://anaconda.org/ranaroussi/yfinance).

### Requirements

- [Python](https://www.python.org) \>= 2.7, 3.4+
- [Pandas](https://github.com/pydata/pandas) \>= 1.3.0
- [Numpy](http://www.numpy.org) \>= 1.16.5
- [requests](http://docs.python-requests.org/en/master) \>= 2.31
- [lxml](https://pypi.org/project/lxml) \>= 4.9.1
- [appdirs](https://pypi.org/project/appdirs) \>= 1.4.4
- [pytz](https://pypi.org/project/pytz) \>=2022.5
- [frozendict](https://pypi.org/project/frozendict) \>= 2.3.4
- [beautifulsoup4](https://pypi.org/project/beautifulsoup4) \>= 4.11.1
- [html5lib](https://pypi.org/project/html5lib) \>= 1.1
- [peewee](https://pypi.org/project/peewee) \>= 3.16.2

#### Optional (if you want to use `pandas_datareader`)

- [pandas\_datareader](https://github.com/pydata/pandas-datareader)
\>= 0.4.0

## Developers: want to contribute?

`yfinance` relies on community to investigate bugs and contribute code. Developer guide: https://github.com/ranaroussi/yfinance/discussions/1084
Expand Down
4 changes: 4 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@
'lxml>=4.9.1', 'appdirs>=1.4.4', 'pytz>=2022.5',
'frozendict>=2.3.4', 'peewee>=3.16.2',
'beautifulsoup4>=4.11.1', 'html5lib>=1.1'],
extras_require={
'nospam': ['requests_cache>=1.1.1', 'requests_ratelimiter>=0.4.2'],
'repair': ['scipy>=1.6.3'],
},
# Note: Pandas.read_html() needs html5lib & beautifulsoup4
entry_points={
'console_scripts': [
Expand Down
58 changes: 56 additions & 2 deletions tests/ticker.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,20 @@
("major_holders", pd.DataFrame),
("institutional_holders", pd.DataFrame),
("mutualfund_holders", pd.DataFrame),
("insider_transactions", pd.DataFrame),
("insider_purchases", pd.DataFrame),
("insider_roster_holders", pd.DataFrame),
("splits", pd.Series),
("actions", pd.DataFrame),
("shares", pd.DataFrame),
("info", dict),
("calendar", pd.DataFrame),
("recommendations", Union[pd.DataFrame, dict]),
("recommendations_summary", Union[pd.DataFrame, dict]),
("upgrades_downgrades", Union[pd.DataFrame, dict]),
("recommendations_history", Union[pd.DataFrame, dict]),
("earnings", pd.DataFrame),
("quarterly_earnings", pd.DataFrame),
("recommendations_summary", Union[pd.DataFrame, dict]),
("quarterly_cashflow", pd.DataFrame),
("cashflow", pd.DataFrame),
("quarterly_balance_sheet", pd.DataFrame),
Expand Down Expand Up @@ -347,6 +352,30 @@ def test_mutualfund_holders(self):
data_cached = self.ticker.mutualfund_holders
self.assertIs(data, data_cached, "data not cached")

def test_insider_transactions(self):
data = self.ticker.insider_transactions
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
self.assertFalse(data.empty, "data is empty")

data_cached = self.ticker.insider_transactions
self.assertIs(data, data_cached, "data not cached")

def test_insider_purchases(self):
data = self.ticker.insider_purchases
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
self.assertFalse(data.empty, "data is empty")

data_cached = self.ticker.insider_purchases
self.assertIs(data, data_cached, "data not cached")

def test_insider_roster_holders(self):
data = self.ticker.insider_roster_holders
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
self.assertFalse(data.empty, "data is empty")

data_cached = self.ticker.insider_roster_holders
self.assertIs(data, data_cached, "data not cached")


class TestTickerMiscFinancials(unittest.TestCase):
session = None
Expand Down Expand Up @@ -645,14 +674,27 @@ def test_recommendations(self):
data_cached = self.ticker.recommendations
self.assertIs(data, data_cached, "data not cached")

# def test_recommendations_summary(self):
# def test_recommendations_summary(self): # currently alias for recommendations
# data = self.ticker.recommendations_summary
# self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
# self.assertFalse(data.empty, "data is empty")

# data_cached = self.ticker.recommendations_summary
# self.assertIs(data, data_cached, "data not cached")

def test_recommendations_history(self): # alias for upgrades_downgrades
data = self.ticker.upgrades_downgrades
data_history = self.ticker.recommendations_history
self.assertTrue(data.equals(data_history))
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
self.assertFalse(data.empty, "data is empty")
self.assertTrue(len(data.columns) == 4, "data has wrong number of columns")
self.assertEqual(data.columns.values.tolist(), ['Firm', 'ToGrade', 'FromGrade', 'Action'], "data has wrong column names")
self.assertIsInstance(data.index, pd.DatetimeIndex, "data has wrong index type")

data_cached = self.ticker.upgrades_downgrades
self.assertIs(data, data_cached, "data not cached")

# def test_analyst_price_target(self):
# data = self.ticker.analyst_price_target
# self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
Expand Down Expand Up @@ -720,6 +762,18 @@ def test_info(self):
self.assertIn("symbol", data.keys(), f"Did not find expected key '{k}' in info dict")
self.assertEqual(self.symbols[0], data["symbol"], "Wrong symbol value in info dict")

def test_complementary_info(self):
# This test is to check that we can successfully retrieve the trailing PEG ratio

# We don't expect this one to have a trailing PEG ratio
data1 = self.tickers[0].info
self.assertEqual(data1['trailingPegRatio'], None)

# This one should have a trailing PEG ratio
data2 = self.tickers[2].info
self.assertEqual(data2['trailingPegRatio'], 1.2713)
pass

# def test_fast_info_matches_info(self):
# fast_info_keys = set()
# for ticker in self.tickers:
Expand Down
42 changes: 39 additions & 3 deletions yfinance/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1718,6 +1718,21 @@ def get_recommendations(self, proxy=None, as_dict=False):
return data.to_dict()
return data

def get_recommendations_summary(self, proxy=None, as_dict=False):
return self.get_recommendations(proxy=proxy, as_dict=as_dict)

def get_upgrades_downgrades(self, proxy=None, as_dict=False):
"""
Returns a DataFrame with the recommendations changes (upgrades/downgrades)
Index: date of grade
Columns: firm toGrade fromGrade action
"""
self._quote.proxy = proxy or self.proxy
data = self._quote.upgrades_downgrades
if as_dict:
return data.to_dict()
return data

def get_calendar(self, proxy=None, as_dict=False):
self._quote.proxy = proxy or self.proxy
data = self._quote.calendar
Expand Down Expand Up @@ -1747,6 +1762,30 @@ def get_mutualfund_holders(self, proxy=None, as_dict=False):
if as_dict:
return data.to_dict()
return data

def get_insider_purchases(self, proxy=None, as_dict=False):
self._holders.proxy = proxy or self.proxy
data = self._holders.insider_purchases
if data is not None:
if as_dict:
return data.to_dict()
return data

def get_insider_transactions(self, proxy=None, as_dict=False):
self._holders.proxy = proxy or self.proxy
data = self._holders.insider_transactions
if data is not None:
if as_dict:
return data.to_dict()
return data

def get_insider_roster_holders(self, proxy=None, as_dict=False):
self._holders.proxy = proxy or self.proxy
data = self._holders.insider_roster
if data is not None:
if as_dict:
return data.to_dict()
return data

def get_info(self, proxy=None) -> dict:
self._quote.proxy = proxy or self.proxy
Expand All @@ -1770,9 +1809,6 @@ def get_sustainability(self, proxy=None, as_dict=False):
return data.to_dict()
return data

def get_recommendations_summary(self, proxy=None, as_dict=False):
return self.get_recommendations(proxy=proxy, as_dict=as_dict)

def get_analyst_price_target(self, proxy=None, as_dict=False):
self._analysis.proxy = proxy or self.proxy
data = self._analysis.analyst_price_target
Expand Down
Loading

0 comments on commit 2f143c0

Please sign in to comment.