Compare commits

..

128 Commits

Author SHA1 Message Date
ValueRaider
cdae1cf226 Bump version to 0.1.86 2022-11-14 12:49:43 +00:00
ValueRaider
bca569318e Merge pull request #1170 from ranaroussi/patch/default-start
Backport #1169 (default start)
2022-11-13 11:52:51 +00:00
ValueRaider
d11cd85a66 Backport #1169 (default start) 2022-11-13 11:51:41 +00:00
ValueRaider
2d32a6e204 Merge pull request #1162 from ranaroussi/patch/tz-csv-error
Fix corrupt tkr-tz-csv halting code
2022-11-10 21:51:10 +00:00
ValueRaider
1687ae66ab Fix corrupt tkr-tz-csv halting code 2022-11-10 14:19:21 +00:00
ValueRaider
ddc34348d9 Merge pull request #1142 from ranaroussi/patch-0.1/delisted-tkr-errors
Improve handling delisted tickers
2022-11-03 22:56:16 +00:00
ValueRaider
1d74cfeb19 Merge pull request #1141 from ranaroussi/patch-0.1/trailing-peg-ratio
Move get 'trailingPegRatio' into _get_info(), simplify & optimise
2022-11-03 22:55:39 +00:00
ValueRaider
1589d07b56 Move get 'trailingPegRatio' into _get_info(), simplify & optimise 2022-11-03 22:53:04 +00:00
ValueRaider
d261237320 Improve handling delisted tickers 2022-11-03 22:49:12 +00:00
ValueRaider
66af3080dd Bump version to 0.1.85 2022-11-03 19:04:45 +00:00
ValueRaider
9d396b9559 Merge pull request #1135 from ranaroussi/patch/unknown-ticker-timezone
Backport ticker tz verification for nice error
2022-11-02 15:18:26 +00:00
ValueRaider
23b6ad12c1 Backport ticker tz verification for nice error 2022-10-31 21:14:50 +00:00
ValueRaider
22131e9fc7 Merge pull request #1124 from Jossan84/main
Bugfix: Get logo url when no website exists
2022-10-27 22:34:18 +01:00
ValueRaider
e99e61f95a Bump version to 0.1.84 2022-10-26 00:12:29 +01:00
ValueRaider
a3fe95ea27 Make tz-cache thread-safe 2022-10-26 00:09:23 +01:00
ValueRaider
000cb70bcb Bump version to 0.1.83 2022-10-25 23:23:32 +01:00
ValueRaider
c8d9d06e75 Expose _fetch_ticker_tz() arguments 2022-10-25 23:21:56 +01:00
ValueRaider
a5e07a0375 Bump version to 0.1.82 2022-10-25 23:15:48 +01:00
ValueRaider
a0a12bcf4c Backport _fetch_ticker_tz() 2022-10-25 23:07:48 +01:00
Ran Aroussi
c49cf626bb Update bug_report.md 2022-10-24 11:37:15 +01:00
Ran Aroussi
fa6f3fc537 Update bug_report.md 2022-10-24 11:37:03 +01:00
ValueRaider
34dfe944d9 Bump version to 0.1.81 2022-10-23 19:54:28 +01:00
ValueRaider
9619839bf5 Merge pull request #1108 from ranaroussi/hotfix/cache-on-read-only-system
Fix cache error on read only system
2022-10-23 19:52:41 +01:00
ValueRaider
90e00a71ca Fix missing 'return' 2022-10-23 19:51:09 +01:00
ValueRaider
f525ee2f5e Add README section on tz-cache ; Add set_tz_cache_location() 2022-10-23 19:47:22 +01:00
ValueRaider
ef12c8b600 Catch read-only exceptions during cache write 2022-10-23 19:29:54 +01:00
ValueRaider
42e6d0894e Bump version to 0.1.80 2022-10-22 13:25:59 +01:00
ValueRaider
de1c3c091b Merge pull request #1103 from ranaroussi/hotfix/download-timezones-patch
Fix download(ignore_tz=True) for single ticker
2022-10-22 13:15:52 +01:00
ValueRaider
c6c0fa3347 Fix download(ignore_tz=True) for single ticker 2022-10-22 13:14:35 +01:00
ValueRaider
75c823a72c Merge pull request #1101 from ranaroussi/hotfix/tz-dst-ambiguous
Fix tz-localize when DST-ambiguous
2022-10-21 15:29:52 +01:00
ValueRaider
f1ad8f0061 Fix tz-localize when DST-ambiguous 2022-10-21 12:43:50 +01:00
ValueRaider
b27cc0cf40 Update to 0.1.79 2022-10-18 20:07:40 +01:00
ValueRaider
1d7f8139d6 Fix when Yahoo returns price=NaNs on dividend day 2022-10-18 20:04:08 +01:00
ValueRaider
01ef1bb813 Update to 0.1.78 2022-10-18 12:53:22 +01:00
ValueRaider
1db6be75b8 Merge pull request #1093 from ranaroussi/fix/download-timezones
Add 'ignore_tz' arg to download()
2022-10-18 12:36:00 +01:00
ValueRaider
7902ec8667 Fix empty-df detection and date ordering 2022-10-18 12:31:51 +01:00
ValueRaider
ff42a3ac87 Add 'ignore_tz' arg to download() 2022-10-18 12:22:43 +01:00
ValueRaider
51f2c7301d Bump version again to 0.1.77 to skip bad tag 2022-10-07 22:04:16 +01:00
ValueRaider
632a16670a Bump version to 0.1.76 2022-10-07 21:55:15 +01:00
ValueRaider
fea0dca6f4 Merge pull request #1078 from ranaroussi/fix/info-access-when-unlisted
Fix/info access when unlisted
2022-10-07 21:44:44 +01:00
ValueRaider
c7e95152a0 Tidy code 2022-10-07 17:23:59 +01:00
ValueRaider
a52e972d04 Pretty error check for timezone retrieval 2022-10-07 17:20:58 +01:00
Ran Aroussi
a197d9f78e updated changelog 2022-10-04 12:09:19 +01:00
Ran Aroussi
dbb9bbfbf3 Updated to 0.1.75 2022-10-04 12:01:30 +01:00
ValueRaider
a7b053addd Merge pull request #1067 from ranaroussi/fix/deps
Add new dep 'appdirs' for recent tz-fixes PR
2022-10-01 13:52:10 +01:00
ValueRaider
e8ca256c10 Add new dep 'appdirs' for recent tz-fixes PR 2022-10-01 13:49:02 +01:00
ValueRaider
f651dd1e93 Merge pull request #1048 from yfinance-fork-team/fix/timezone
Fix timezone & datetime handling
2022-09-21 16:53:29 +01:00
ValueRaider
f40cf0aae1 Merge branch 'main' into fix/timezone 2022-09-20 23:28:17 +01:00
ValueRaider
200f57c458 Merge pull request #1033 from yfinance-fork-team/fix/yahoo-appending-latest-price
Bugfix: Relocate out-of-range check to before 15m->30m conversion
2022-09-20 20:24:13 +01:00
ValueRaider
e5d45eaa85 Merge pull request #1032 from yfinance-fork-team/feature/keep-na
Add 'keepna' argument
2022-09-20 14:17:34 +01:00
ValueRaider
42b77a9b54 Merge pull request #1042 from yfinance-fork-team/feature/init-perf-boost
Ticker.init() perf boost
2022-09-20 14:15:50 +01:00
Jose Manuel
42e5751705 Bugfix: Get logo url when no website exists 2022-09-19 13:54:56 +02:00
ValueRaider
bca005a2c0 Move user dt parsing to utils.py 2022-09-04 17:45:37 +01:00
ValueRaider
ca891bb187 Restore index to DatetimeIndex ; remove tz arg ; refactor for clean merge 2022-09-04 16:09:55 +01:00
Value Raider
0939ff3c78 Support user providing epoch ints 2022-08-15 16:04:46 +01:00
Value Raider
6f5c5635be Preemptive bugfix 2022-07-28 12:30:13 +01:00
Value Raider
809622e426 Ticker init does unnecessary work - move it out 2022-07-28 12:25:43 +01:00
Value Raider
eec1f3dbad Fix when Yahoo messes up DST 2022-07-27 21:54:12 +01:00
Value Raider
1de789ad72 Fix timestamp->dt 2022-07-15 00:00:08 +01:00
Value Raider
cd68ff68c6 Adjust prices-after-end-dt fix from > to >= 2022-07-14 23:45:18 +01:00
Value Raider
9673970f45 Relocate out-of-range check to before 15m->30m interpolation 2022-07-14 23:04:41 +01:00
Value Raider
6ea69a70ac Add 'keepna' argument 2022-07-14 14:41:24 +01:00
Value Raider
c723a5ab44 Preemptive perf. boost 2022-07-14 01:03:24 +01:00
Value Raider
50741d1409 Add tz to daily/weekly - bugfix 2022-07-12 23:04:04 +01:00
Value Raider
69d0dcd62b Add tz to daily/weekly 2022-07-12 22:54:01 +01:00
Value Raider
5c9348f255 end time should default=00:00 not end-of-day - fix comment 2022-07-12 21:30:29 +01:00
Value Raider
a472546e7b end time should default=00:00 not end-of-day 2022-07-12 21:30:29 +01:00
Value Raider
c914f1f183 Fix user-supplied TZ ; reduce Git diff 2022-07-12 21:30:29 +01:00
Value Raider
92c82342fe Fix PYTZ misuse 2022-07-12 21:30:29 +01:00
Value Raider
7ae08b04f3 Fix setting end dt to midnight 2022-07-12 21:30:29 +01:00
Value Raider
4b50f1e81c Perf boost: store ticker timezone in user file cache 2022-07-12 21:30:29 +01:00
Value Raider
1ed58be749 Perf boost: separate info fetch from financials, to get tz fast 2022-07-12 21:30:28 +01:00
Value Raider
375b4f9376 Fix error when adding Dividends/Splits to df 2022-07-12 21:27:58 +01:00
Value Raider
b6b4426ca9 User-provided 'tz' should 'convert' not 'replace' UTC 2022-07-12 21:27:58 +01:00
Value Raider
149ebe46db Remove debugging print stmt 2022-07-12 21:27:58 +01:00
Value Raider
d80b27cfde Fix Yahoo returning tz in data 2022-07-12 21:27:58 +01:00
Value Raider
36e277317b Fix date->datetime conversion 2022-07-12 21:27:58 +01:00
Value Raider
0e1ea4d2c6 Replace zoneinfo with pytz (installed by pandas) 2022-07-12 21:27:58 +01:00
Value Raider
2d96c383ef Set start/end timezone to exchange if None 2022-07-12 21:27:57 +01:00
Ran Aroussi
ec6279736b Fixed bug introduced in 0.1.73 2022-07-11 23:20:47 +01:00
Ran Aroussi
5d942d9668 Update CHANGELOG.rst 2022-07-11 12:16:00 +01:00
Ran Aroussi
5782cb59fd Update version.py 2022-07-11 12:15:28 +01:00
Ran Aroussi
4c4861a8f1 Merge pull request #1000 from giorgossideris/shares_outstanding_none_values_bug_fix
Handle case that the API returns no data for shares of a specific year (#999)
2022-07-11 12:14:51 +01:00
Ran Aroussi
4d221ca70e Merge pull request #1009 from AndKraev/fix_724
rename incorrect yahoo key treasuryStock
2022-07-11 12:14:31 +01:00
Ran Aroussi
1a8d045baf Merge pull request #1023 from Ckend/main
bugfix: remove useless arg, otherwise affect the proxy arg.
2022-07-11 12:13:44 +01:00
Ran Aroussi
67a55c35ce Merge pull request #1024 from devpg/patch-1
Update multi.py
2022-07-11 12:13:21 +01:00
Ran Aroussi
e547fe4e41 Merge pull request #1026 from ValueRaider/fix/yahoo-appending-latest-price
Bug fix - Yahoo API appends latest price regardless of end date, remove it
2022-07-11 12:13:04 +01:00
Value Raider
9d5366d707 Bug fix - Yahoo API appends latest price regardless of end date, remove it 2022-07-09 18:35:12 +01:00
André Neubauer
4b07d1dceb Update multi.py
Documentation is wrong here. 'show_errors' does print errors if True, and doesn't print errors if False
2022-07-09 12:01:53 +02:00
ckend
9440c1e1c1 bugfix: remove useless arg, otherwise affect the proxy arg. 2022-07-08 17:53:54 +08:00
Ran Aroussi
773d003a67 Update CHANGELOG.rst 2022-06-16 16:32:33 +01:00
Ran Aroussi
a2905a0f8d Update version.py 2022-06-16 16:32:12 +01:00
Ran Aroussi
1810455e15 bugfix 2022-06-16 16:31:56 +01:00
Ran Aroussi
9770f286b4 v# + clog 2022-06-16 16:24:41 +01:00
Ran Aroussi
80eb0ddafb Merge branch 'main' of github.com:ranaroussi/yfinance 2022-06-16 16:17:16 +01:00
Ran Aroussi
a9e9e8dcb3 added Tickers(…).news() 2022-06-16 16:17:12 +01:00
Ran Aroussi
69c0673345 Merge pull request #959 from Kartheyan/patch-1
Fixed max period when interval is 1 minute
2022-06-16 16:16:53 +01:00
Ran Aroussi
8077cabf44 Merge branch 'main' of github.com:ranaroussi/yfinance 2022-06-16 16:04:32 +01:00
Ran Aroussi
e35b98ef1d pep stuff 2022-06-16 16:04:28 +01:00
Ran Aroussi
dc40c6d093 Merge pull request #955 from alker0/issue-677
Support cache in `Tickers()`
2022-06-16 16:04:20 +01:00
Ran Aroussi
8572628ba6 Merge pull request #1017 from ValueRaider/feature/earnings-dates
Feature - fetch full earnings dates history
2022-06-16 16:00:41 +01:00
Ran Aroussi
f878ce5ea5 Merge branch 'main' into feature/earnings-dates 2022-06-16 16:00:32 +01:00
Ran Aroussi
bbf68daf3f Merge pull request #1014 from rickturner2001/feature-earnings-history
Added get_earnings_history to fetch earnings data
2022-06-16 15:51:54 +01:00
Value Raider
afbb5d81a4 Tidy utils change 2022-06-16 12:12:04 +01:00
Value Raider
670ec8e766 Return empty DF if YF missing earnings dates 2022-06-16 12:08:16 +01:00
Value Raider
8fa9438072 Fix EPS % to 0->1 2022-06-16 12:07:15 +01:00
Value Raider
8c56a0c3df Fix timezone handling 2022-06-15 21:17:33 +01:00
Value Raider
6e90fc17cb Fix handling of missing data 2022-06-15 19:57:24 +01:00
Value Raider
0849f68dd8 Clean&format earnings_dates table 2022-06-15 19:36:54 +01:00
Value Raider
77f3810cdd Add feature get_earnings_dates() 2022-06-15 17:42:22 +01:00
rickturner01
f3583b00e3 Added function to retreive earnings calendar 2022-06-11 22:23:15 -04:00
rickturner01
d0606cdb03 Added get_earnings_history to fetch earnings data 2022-06-11 02:50:42 -04:00
andKraev
76a9b09e8e rename incorrect yahoo key treasuryStock 2022-05-27 16:53:17 +03:00
giorgossideris
d757b8f25f Handle case that the API returns no data for shares of a specific year 2022-05-09 15:24:10 +03:00
Kartheyan
ab50edf24c Fixed max period when interval is 1 minute
When attempting to retrieve the maximum period of time (period="max") at 1 minute intervals, the previous default start period of 1900-01-01 (631159200) would result in an error from the API with description "1m data not available for startTime=1497744000 and endTime=*NOW*. Only 7 days worth of 1m granularity data are allowed to be fetched per request." . By setting the start time to subtracting end time minus 604800 (86400*7), it now returns 1 week of 1 minute interval data which is the maximum period for which the API supports at 1 minute intervals.
2022-02-15 15:27:37 +00:00
alker0
9b4bce7d06 Support cache in Tickers() 2022-02-09 14:51:11 +09:00
Ran Aroussi
a396d93601 Update CHANGELOG.rst 2022-01-30 12:13:53 +00:00
Ran Aroussi
79d6741f1e Update version.py 2022-01-30 12:13:15 +00:00
Ran Aroussi
47a119f63b Update setup.py 2022-01-27 16:35:53 +00:00
Ran Aroussi
436c077ee2 Update requirements.txt 2022-01-27 16:35:43 +00:00
Ran Aroussi
72b1e9699e Merge pull request #941 from moshfrid/main
Explicitly set datetime index on actions table
2022-01-27 16:34:50 +00:00
Ran Aroussi
d2c2690cc2 Merge pull request #943 from rafaelsdellama/main
Fix: dev: AttributeError
2022-01-27 16:33:13 +00:00
Rafael Del Lama
65c1753fb2 lock pandas version on setup.py 2022-01-27 13:18:09 -03:00
Rafael Del Lama
93edc3c163 Fix: dev: AttributeError
Lock pandas version to fix AttributeError: 'Index' object has no attribute 'tz_localize' after pandas upgrade version to 1.4.0
2022-01-27 12:03:26 -03:00
moshfrid
b2e2acad06 Explicitly set datetime index on actions table
Explicitly set datetime index on actions df so that if the actions df is empty, it doesn't reset the index when combined with the rest of the dataframe.
2022-01-24 19:14:12 -05:00
ran
24e348e0e4 merged pr #920 2022-01-12 15:43:10 +00:00
Ran Aroussi
0ac69cfd49 Merge pull request #920 from tsherknus/main
Resolves UnboundLocalError in base.py history()
2022-01-12 15:41:34 +00:00
tsherknus
cdced94a00 Resolves UnboundLocalError in base.py history()
In base.py history(), session.get can return None or 404. Clarify exception catching within the history() method of base.py.

Ensure none and 404 values are caught prior to quote processing.

This prevents json parse exception, connection exception, or unbound exception from being thrown during data collection.

Includes updated unit testing.
2022-01-11 18:51:12 -05:00
14 changed files with 756 additions and 229 deletions

View File

@@ -14,4 +14,7 @@ Before posting an issue - please upgrade to the latest version and confirm the i
Upgrade using:
`$ pip install yfinance --upgrade --no-cache-dir`
Bug still there? Delete this content and submit your bug report here...
Bug still there? Delete this content and submit your bug report here and provide the following, as best you can:
- Simple code that reproduces your problem
- The error message

View File

@@ -1,6 +1,85 @@
Change Log
===========
0.1.86
------
- Fix 'trailingPegRatio' #1141
- Improve handling delisted tickers #1142
- Fix corrupt tkr-tz-csv halting code #1162
- Change default start to 1900-01-01 #1170
0.1.85
------
- Fix info['log_url'] #1062
- Fix handling delisted ticker #1137
0.1.84
------
- Make tz-cache thread-safe
0.1.83
------
- Reduce spam-effect of tz-fetch
0.1.81
------
- Fix unhandled tz-cache exception #1107
0.1.80
------
- Fix `download(ignore_tz=True)` for single ticker #1097
- Fix rare case of error "Cannot infer DST time" #1100
0.1.79
------
- Fix when Yahoo returns price=NaNs on dividend day
0.1.78
------
- Fix download() when different timezones #1085
0.1.77
------
- Fix user experience bug #1078
0.1.75
------
- Fixed datetime-related issues: #1048
- Add 'keepna' argument #1032
- Speedup Ticker() creation #1042
- Improve a bugfix #1033
0.1.74
------
- Fixed bug introduced in 0.1.73 (sorry :/)
0.1.73
------
- Merged several PR that fixed misc issues
0.1.72
------
- Misc bugfixs
0.1.71
------
- Added Tickers(…).news()
- Return empty DF if YF missing earnings dates
- Fix EPS % to 0->1
- Fix timezone handling
- Fix handling of missing data
- Clean&format earnings_dates table
- Add ``.get_earnings_dates()`` to retreive earnings calendar
- Added ``.get_earnings_history()`` to fetch earnings data
0.1.70
------
- Bug fixed - Closes #937
0.1.69
------
- Bug fixed - #920
0.1.68
------
- Upgraded requests dependency

View File

@@ -48,8 +48,6 @@ Yahoo! finance API is intended for personal use only.**
The `Ticker` module, which allows you to access ticker data in a more Pythonic way:
Note: yahoo finance datetimes are received as UTC.
```python
import yfinance as yf
@@ -101,6 +99,9 @@ msft.recommendations
# show next event (earnings, etc)
msft.calendar
# show all earnings dates
msft.earnings_dates
# show ISIN code - *experimental*
# ISIN = International Securities Identification Number
msft.isin
@@ -184,6 +185,11 @@ data = yf.download( # or pdr.get_data_yahoo(...
# (optional, default is '1d')
interval = "1m",
# Whether to ignore timezone when aligning ticker data from
# different timezones. Default is True. False may be useful for
# minute/hourly data.
ignore_tz = False,
# group by ticker (to access via data['SPY'])
# (optional, default is 'column')
group_by = 'ticker',
@@ -206,6 +212,18 @@ data = yf.download( # or pdr.get_data_yahoo(...
)
```
### Timezone cache store
When fetching price data, all dates are localized to stock exchange timezone.
But timezone retrieval is relatively slow, so yfinance attemps to cache them
in your users cache folder.
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")
...
```
### Managing Multi-Level Columns
The following answer on Stack Overflow is for [How to deal with
@@ -261,6 +279,7 @@ To install `yfinance` using `conda`, see
- [Numpy](http://www.numpy.org) \>= 1.11.1
- [requests](http://docs.python-requests.org/en/master/) \>= 2.14.2
- [lxml](https://pypi.org/project/lxml/) \>= 4.5.1
- [appdirs](https://pypi.org/project/appdirs) \>=1.4.4
### Optional (if you want to use `pandas_datareader`)

View File

@@ -21,6 +21,7 @@ requirements:
- requests >=2.21
- multitasking >=0.0.7
- lxml >=4.5.1
- appdirs >= 1.4.4
- pip
- python
@@ -30,6 +31,7 @@ requirements:
- requests >=2.21
- multitasking >=0.0.7
- lxml >=4.5.1
- appdirs >= 1.4.4
- python
test:

View File

@@ -3,3 +3,4 @@ numpy>=1.16.5
requests>=2.26
multitasking>=0.0.7
lxml>=4.5.1
appdirs>=1.4.4

View File

@@ -61,9 +61,9 @@ setup(
platforms=['any'],
keywords='pandas, yahoo finance, pandas datareader',
packages=find_packages(exclude=['contrib', 'docs', 'tests', 'examples']),
install_requires=['pandas>=0.24', 'numpy>=1.15',
install_requires=['pandas>=0.24.0', 'numpy>=1.15',
'requests>=2.26', 'multitasking>=0.0.7',
'lxml>=4.5.1'],
'lxml>=4.5.1', 'appdirs>=1.4.4'],
entry_points={
'console_scripts': [
'sample=sample:main',

View File

@@ -15,20 +15,83 @@ Sanity check for most common library uses all working
import yfinance as yf
import unittest
import datetime
session = None
import requests_cache ; session = requests_cache.CachedSession("yfinance.cache", expire_after=24*60*60)
symbols = ['MSFT', 'IWO', 'VFINX', '^GSPC', 'BTC-USD']
tickers = [yf.Ticker(symbol) for symbol in symbols]
tickers = [yf.Ticker(symbol, session=session) for symbol in symbols]
delisted_symbols = ["BRK.B", "SDLP"]
delisted_tickers = [yf.Ticker(symbol, session=session) for symbol in delisted_symbols]
class TestTicker(unittest.TestCase):
def setUp(self):
d_today = datetime.date.today()
d_today -= datetime.timedelta(days=30)
self.start_d = datetime.date(d_today.year, d_today.month, 1)
def test_info_history(self):
# always should have info and history for valid symbols
for ticker in tickers:
# always should have info and history for valid symbols
assert(ticker.info is not None and ticker.info != {})
assert(ticker.history(period="max").empty is False)
history = ticker.history(period="1mo")
assert(history.empty is False and history is not None)
histories = yf.download(symbols, period="1mo", session=session)
assert(histories.empty is False and histories is not None)
for ticker in tickers:
assert(ticker.info is not None and ticker.info != {})
history = ticker.history(start=self.start_d)
assert(history.empty is False and history is not None)
histories = yf.download(symbols, start=self.start_d, session=session)
assert(histories.empty is False and histories is not None)
def test_info_history_nofail(self):
# should not throw Exception for delisted tickers, just print a message
for ticker in delisted_tickers:
history = ticker.history(period="1mo")
histories = yf.download(delisted_symbols, period="1mo", session=session)
histories = yf.download(delisted_symbols[0], period="1mo", session=session)
histories = yf.download(delisted_symbols[1], period="1mo")#, session=session)
for ticker in delisted_tickers:
history = ticker.history(start=self.start_d)
histories = yf.download(delisted_symbols, start=self.start_d, session=session)
histories = yf.download(delisted_symbols[0], start=self.start_d, session=session)
histories = yf.download(delisted_symbols[1], start=self.start_d, session=session)
def test_attributes(self):
for ticker in tickers:
ticker.isin
ticker.major_holders
ticker.institutional_holders
ticker.mutualfund_holders
ticker.dividends
ticker.splits
ticker.actions
ticker.info
ticker.info["trailingPegRatio"]
ticker.calendar
ticker.recommendations
ticker.earnings
ticker.quarterly_earnings
ticker.financials
ticker.quarterly_financials
ticker.balance_sheet
ticker.quarterly_balance_sheet
ticker.cashflow
ticker.quarterly_cashflow
ticker.sustainability
ticker.options
ticker.news
ticker.shares
ticker.earnings_history
ticker.earnings_dates
def test_attributes_nofail(self):
# should not throw Exception for delisted tickers, just print a message
for ticker in delisted_tickers:
ticker.isin
ticker.major_holders
ticker.institutional_holders
@@ -51,6 +114,8 @@ class TestTicker(unittest.TestCase):
ticker.options
ticker.news
ticker.shares
ticker.earnings_history
ticker.earnings_dates
def test_holders(self):
for ticker in tickers:

View File

@@ -23,6 +23,7 @@ from . import version
from .ticker import Ticker
from .tickers import Tickers
from .multi import download
from .utils import set_tz_cache_location
__version__ = version.version
__author__ = "Ran Aroussi"
@@ -42,4 +43,4 @@ def pdr_override():
pass
__all__ = ['download', 'Ticker', 'Tickers', 'pdr_override']
__all__ = ['download', 'Ticker', 'Tickers', 'pdr_override', 'set_tz_cache_location']

View File

@@ -23,6 +23,7 @@ from __future__ import print_function
import time as _time
import datetime as _datetime
import pytz as _tz
import requests as _requests
import pandas as _pd
import numpy as _np
@@ -43,6 +44,8 @@ from . import shared
_BASE_URL_ = 'https://query2.finance.yahoo.com'
_SCRAPE_URL_ = 'https://finance.yahoo.com/quote'
_ROOT_URL_ = 'https://finance.yahoo.com'
class TickerBase():
def __init__(self, ticker, session=None):
@@ -51,6 +54,7 @@ class TickerBase():
self._history = None
self._base_url = _BASE_URL_
self._scrape_url = _SCRAPE_URL_
self._tz = None
self._fundamentals = False
self._info = None
@@ -66,19 +70,13 @@ class TickerBase():
self._calendar = None
self._expirations = {}
self._earnings_dates = None
self._earnings_history = None
self._earnings = {
"yearly": utils.empty_df(),
"quarterly": utils.empty_df()}
self._financials = {
"yearly": utils.empty_df(),
"quarterly": utils.empty_df()}
self._balancesheet = {
"yearly": utils.empty_df(),
"quarterly": utils.empty_df()}
self._cashflow = {
"yearly": utils.empty_df(),
"quarterly": utils.empty_df()}
self._earnings = None
self._financials = None
self._balancesheet = None
self._cashflow = None
# accept isin as ticker
if utils.is_isin(self.ticker):
@@ -102,8 +100,8 @@ class TickerBase():
def history(self, period="1mo", interval="1d",
start=None, end=None, prepost=False, actions=True,
auto_adjust=True, back_adjust=False,
proxy=None, rounding=False, tz=None, timeout=None, **kwargs):
auto_adjust=True, back_adjust=False, keepna=False,
proxy=None, rounding=False, timeout=None, **kwargs):
"""
:Parameters:
period : str
@@ -125,14 +123,14 @@ class TickerBase():
Adjust all OHLC automatically? Default is True
back_adjust: bool
Back-adjusted data to mimic true historical prices
keepna: bool
Keep NaN rows returned by Yahoo?
Default is False
proxy: str
Optional. Proxy server URL scheme. Default is None
rounding: bool
Round values to 2 decimal places?
Optional. Default is False = precision suggested by Yahoo!
tz: str
Optional timezone locale for dates.
(default data is returned as non-localized dates)
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)
@@ -143,21 +141,36 @@ class TickerBase():
error message printing to console.
"""
# Work with errors
debug_mode = True
if "debug" in kwargs and isinstance(kwargs["debug"], bool):
debug_mode = kwargs["debug"]
err_msg = "No data found for this date range, symbol may be delisted"
if start or period is None or period.lower() == "max":
if start is None:
start = -631159200
elif isinstance(start, _datetime.datetime):
start = int(_time.mktime(start.timetuple()))
else:
start = int(_time.mktime(
_time.strptime(str(start), '%Y-%m-%d')))
# Check can get TZ. Fail => probably delisted
tz = self._get_ticker_tz(debug_mode, proxy, timeout)
if tz is None:
# Every valid ticker has a timezone. Missing = problem
shared._DFS[self.ticker] = utils.empty_df()
shared._ERRORS[self.ticker] = err_msg
if "many" not in kwargs and debug_mode:
print('- %s: %s' % (self.ticker, err_msg))
return utils.empty_df()
if end is None:
end = int(_time.time())
elif isinstance(end, _datetime.datetime):
end = int(_time.mktime(end.timetuple()))
else:
end = int(_time.mktime(_time.strptime(str(end), '%Y-%m-%d')))
end = utils._parse_user_dt(end, tz)
if start is None:
if interval == "1m":
start = end - 604800 # Subtract 7 days
else:
#time stamp of 01/01/1900
start = -2208994789
else:
start = utils._parse_user_dt(start, tz)
params = {"period1": start, "period2": end}
else:
period = period.lower()
@@ -181,6 +194,7 @@ class TickerBase():
url = "{}/v8/finance/chart/{}".format(self._base_url, self.ticker)
session = self.session or _requests
data = None
try:
data = session.get(
@@ -199,12 +213,13 @@ class TickerBase():
except Exception:
pass
# Work with errors
debug_mode = True
if "debug" in kwargs and isinstance(kwargs["debug"], bool):
debug_mode = kwargs["debug"]
if data is None or not type(data) is dict or 'status_code' in data.keys():
shared._DFS[self.ticker] = utils.empty_df()
shared._ERRORS[self.ticker] = err_msg
if "many" not in kwargs and debug_mode:
print('- %s: %s' % (self.ticker, err_msg))
return utils.empty_df()
err_msg = "No data found for this date range, symbol may be delisted"
if "chart" in data and data["chart"]["error"]:
err_msg = data["chart"]["error"]["description"]
shared._DFS[self.ticker] = utils.empty_df()
@@ -223,7 +238,12 @@ class TickerBase():
# parse quotes
try:
quotes = utils.parse_quotes(data["chart"]["result"][0], tz)
quotes = utils.parse_quotes(data["chart"]["result"][0])
# Yahoo bug fix - it often appends latest price even if after end date
if end and not quotes.empty:
endDt = _pd.to_datetime(_datetime.datetime.utcfromtimestamp(end))
if quotes.index[quotes.shape[0]-1] >= endDt:
quotes = quotes.iloc[0:quotes.shape[0]-1]
except Exception:
shared._DFS[self.ticker] = utils.empty_df()
shared._ERRORS[self.ticker] = err_msg
@@ -271,10 +291,10 @@ class TickerBase():
"chart"]["result"][0]["meta"]["priceHint"])
quotes['Volume'] = quotes['Volume'].fillna(0).astype(_np.int64)
quotes.dropna(inplace=True)
# actions
dividends, splits = utils.parse_actions(data["chart"]["result"][0], tz)
dividends, splits = utils.parse_actions(data["chart"]["result"][0])
tz_exchange = data["chart"]["result"][0]["meta"]["exchangeTimezoneName"]
# combine
df = _pd.concat([quotes, dividends, splits], axis=1, sort=True)
@@ -282,33 +302,242 @@ class TickerBase():
df["Stock Splits"].fillna(0, inplace=True)
# index eod/intraday
df.index = df.index.tz_localize("UTC").tz_convert(
data["chart"]["result"][0]["meta"]["exchangeTimezoneName"])
df.index = df.index.tz_localize("UTC").tz_convert(tz_exchange)
df = utils.fix_Yahoo_dst_issue(df, params["interval"])
if params["interval"][-1] == "m":
df.index.name = "Datetime"
elif params["interval"] == "1h":
pass
else:
df.index = _pd.to_datetime(df.index.date)
if tz is not None:
df.index = df.index.tz_localize(tz)
# If a midnight is during DST transition hour when clocks roll back,
# meaning clock hits midnight twice, then use the 2nd (ambiguous=True)
df.index = _pd.to_datetime(df.index.date).tz_localize(tz_exchange, ambiguous=True)
df.index.name = "Date"
# duplicates and missing rows cleanup
df.dropna(how='all', inplace=True)
df = df[~df.index.duplicated(keep='first')]
self._history = df.copy()
if not actions:
df.drop(columns=["Dividends", "Stock Splits"], inplace=True)
df = df.drop(columns=["Dividends", "Stock Splits"])
if not keepna:
mask_nan_or_zero = (df.isna()|(df==0)).all(axis=1)
df = df.drop(mask_nan_or_zero.index[mask_nan_or_zero])
return df
# ------------------------
def _get_fundamentals(self, kind=None, proxy=None):
def _get_ticker_tz(self, debug_mode, proxy, timeout):
if not self._tz is None:
return self._tz
tkr_tz = utils.cache_lookup_tkr_tz(self.ticker)
if tkr_tz is not None:
invalid_value = isinstance(tkr_tz, str)
if not invalid_value:
try:
_tz.timezone(tz)
except:
invalid_value = True
if invalid_value:
# Clear from cache and force re-fetch
utils.cache_store_tkr_tz(self.ticker, None)
tkr_tz = None
if tkr_tz is None:
tkr_tz = self._fetch_ticker_tz(debug_mode, proxy, timeout)
if tkr_tz is not None:
try:
utils.cache_store_tkr_tz(self.ticker, tkr_tz)
except PermissionError:
# System probably read-only, so cannot cache
pass
self._tz = tkr_tz
return tkr_tz
def _fetch_ticker_tz(self, debug_mode, proxy, timeout):
# Query Yahoo for basic price data just to get returned timezone
params = {"range":"1d", "interval":"1d"}
# setup proxy in requests format
if proxy is not None:
if isinstance(proxy, dict) and "https" in proxy:
proxy = proxy["https"]
proxy = {"https": proxy}
# Getting data from json
url = "{}/v8/finance/chart/{}".format(self._base_url, self.ticker)
session = self.session or _requests
try:
data = session.get(url=url, params=params, proxies=proxy, headers=utils.user_agent_headers, timeout=timeout)
data = data.json()
except Exception as e:
if debug_mode:
print("Failed to get ticker '{}' reason: {}".format(self.ticker, e))
return None
else:
error = data.get('chart', {}).get('error', None)
if error:
# explicit error from yahoo API
if debug_mode:
print("Got error from yahoo api for ticker {}, Error: {}".format(self.ticker, error))
else:
try:
return data["chart"]["result"][0]["meta"]["exchangeTimezoneName"]
except Exception as err:
if debug_mode:
print("Could not get exchangeTimezoneName for ticker '{}' reason: {}".format(self.ticker, err))
print("Got response: ")
print("-------------")
print(" {}".format(data))
print("-------------")
return None
def _get_info(self, proxy=None):
# setup proxy in requests format
if proxy is not None:
if isinstance(proxy, dict) and "https" in proxy:
proxy = proxy["https"]
proxy = {"https": proxy}
if (self._info is None) or (self._sustainability is None) or (self._recommendations is None):
## Need to fetch
pass
else:
return
ticker_url = "{}/{}".format(self._scrape_url, self.ticker)
# get info and sustainability
data = utils.get_json(ticker_url, proxy, self.session)
# sustainability
d = {}
try:
if isinstance(data.get('esgScores'), dict):
for item in data['esgScores']:
if not isinstance(data['esgScores'][item], (dict, list)):
d[item] = data['esgScores'][item]
s = _pd.DataFrame(index=[0], data=d)[-1:].T
s.columns = ['Value']
s.index.name = '%.f-%.f' % (
s[s.index == 'ratingYear']['Value'].values[0],
s[s.index == 'ratingMonth']['Value'].values[0])
self._sustainability = s[~s.index.isin(
['maxAge', 'ratingYear', 'ratingMonth'])]
except Exception:
pass
# info (be nice to python 2)
self._info = {}
try:
items = ['summaryProfile', 'financialData', 'quoteType',
'defaultKeyStatistics', 'assetProfile', 'summaryDetail']
for item in items:
if isinstance(data.get(item), dict):
self._info.update(data[item])
except Exception:
pass
# For ETFs, provide this valuable data: the top holdings of the ETF
try:
if 'topHoldings' in data:
self._info.update(data['topHoldings'])
except Exception:
pass
try:
if not isinstance(data.get('summaryDetail'), dict):
# For some reason summaryDetail did not give any results. The price dict usually has most of the same info
self._info.update(data.get('price', {}))
except Exception:
pass
try:
# self._info['regularMarketPrice'] = self._info['regularMarketOpen']
self._info['regularMarketPrice'] = data.get('price', {}).get(
'regularMarketPrice', self._info.get('regularMarketOpen', None))
except Exception:
pass
try:
self._info['preMarketPrice'] = data.get('price', {}).get(
'preMarketPrice', self._info.get('preMarketPrice', None))
except Exception:
pass
self._info['logo_url'] = ""
try:
if not 'website' in self._info:
self._info['logo_url'] = 'https://logo.clearbit.com/%s.com' % self._info['shortName'].split(' ')[0].split(',')[0]
else:
domain = self._info['website'].split(
'://')[1].split('/')[0].replace('www.', '')
self._info['logo_url'] = 'https://logo.clearbit.com/%s' % domain
except Exception:
pass
# events
try:
cal = _pd.DataFrame(
data['calendarEvents']['earnings'])
cal['earningsDate'] = _pd.to_datetime(
cal['earningsDate'], unit='s')
self._calendar = cal.T
self._calendar.index = utils.camel2title(self._calendar.index)
self._calendar.columns = ['Value']
except Exception:
pass
# analyst recommendations
try:
rec = _pd.DataFrame(
data['upgradeDowngradeHistory']['history'])
rec['earningsDate'] = _pd.to_datetime(
rec['epochGradeDate'], unit='s')
rec.set_index('earningsDate', inplace=True)
rec.index.name = 'Date'
rec.columns = utils.camel2title(rec.columns)
self._recommendations = rec[[
'Firm', 'To Grade', 'From Grade', 'Action']].sort_index()
except Exception:
pass
# Complementary key-statistics. For now just want 'trailing PEG ratio'
session = self.session or _requests
keys = {"trailingPegRatio"}
if len(keys)>0:
# For just one/few variable is faster to query directly:
url = "https://query1.finance.yahoo.com/ws/fundamentals-timeseries/v1/finance/timeseries/{}?symbol={}".format(self.ticker, self.ticker)
for k in keys:
url += "&type="+k
# Request 6 months of data
url += "&period1={}".format(int((_datetime.datetime.now()-_datetime.timedelta(days=365//2)).timestamp()))
url += "&period2={}".format(int((_datetime.datetime.now()+_datetime.timedelta(days=1)).timestamp()))
json_str = session.get(url=url, proxies=proxy, headers=utils.user_agent_headers).text
json_data = _json.loads(json_str)
key_stats = json_data["timeseries"]["result"][0]
if k not in key_stats:
# Yahoo website prints N/A, indicates Yahoo lacks necessary data to calculate
v = None
else:
# Select most recent (last) raw value in list:
v = key_stats[k][-1]["reportedValue"]["raw"]
self._info[k] = v
def _get_fundamentals(self, proxy=None):
def cleanup(data):
df = _pd.DataFrame(data).drop(columns=['maxAge'])
for col in df.columns:
@@ -324,6 +553,9 @@ class TickerBase():
df.columns.name = ''
df.index.name = 'Breakdown'
# rename incorrect yahoo key
df.rename(index={'treasuryStock': 'Gains Losses Not Affecting Retained Earnings'}, inplace=True)
df.index = utils.camel2title(df.index)
return df
@@ -338,9 +570,6 @@ class TickerBase():
ticker_url = "{}/{}".format(self._scrape_url, self.ticker)
# get info and sustainability
data = utils.get_json(ticker_url, proxy, self.session)
# holders
try:
resp = utils.get_html(ticker_url + '/holders', proxy, self.session)
@@ -377,101 +606,16 @@ class TickerBase():
self._mutualfund_holders['% Out'] = self._mutualfund_holders[
'% Out'].str.replace('%', '').astype(float) / 100
# sustainability
d = {}
try:
if isinstance(data.get('esgScores'), dict):
for item in data['esgScores']:
if not isinstance(data['esgScores'][item], (dict, list)):
d[item] = data['esgScores'][item]
s = _pd.DataFrame(index=[0], data=d)[-1:].T
s.columns = ['Value']
s.index.name = '%.f-%.f' % (
s[s.index == 'ratingYear']['Value'].values[0],
s[s.index == 'ratingMonth']['Value'].values[0])
self._sustainability = s[~s.index.isin(
['maxAge', 'ratingYear', 'ratingMonth'])]
except Exception:
pass
# info (be nice to python 2)
self._info = {}
try:
items = ['summaryProfile', 'financialData', 'quoteType',
'defaultKeyStatistics', 'assetProfile', 'summaryDetail']
for item in items:
if isinstance(data.get(item), dict):
self._info.update(data[item])
except Exception:
pass
# For ETFs, provide this valuable data: the top holdings of the ETF
try:
if 'topHoldings' in data:
self._info.update(data['topHoldings'])
except Exception:
pass
try:
if not isinstance(data.get('summaryDetail'), dict):
# For some reason summaryDetail did not give any results. The price dict usually has most of the same info
self._info.update(data.get('price', {}))
except Exception:
pass
try:
# self._info['regularMarketPrice'] = self._info['regularMarketOpen']
self._info['regularMarketPrice'] = data.get('price', {}).get(
'regularMarketPrice', self._info.get('regularMarketOpen', None))
except Exception:
pass
try:
self._info['preMarketPrice'] = data.get('price', {}).get(
'preMarketPrice', self._info.get('preMarketPrice', None))
except Exception:
pass
self._info['logo_url'] = ""
try:
domain = self._info['website'].split(
'://')[1].split('/')[0].replace('www.', '')
self._info['logo_url'] = 'https://logo.clearbit.com/%s' % domain
except Exception:
pass
# events
try:
cal = _pd.DataFrame(
data['calendarEvents']['earnings'])
cal['earningsDate'] = _pd.to_datetime(
cal['earningsDate'], unit='s')
self._calendar = cal.T
self._calendar.index = utils.camel2title(self._calendar.index)
self._calendar.columns = ['Value']
except Exception:
pass
# analyst recommendations
try:
rec = _pd.DataFrame(
data['upgradeDowngradeHistory']['history'])
rec['earningsDate'] = _pd.to_datetime(
rec['epochGradeDate'], unit='s')
rec.set_index('earningsDate', inplace=True)
rec.index.name = 'Date'
rec.columns = utils.camel2title(rec.columns)
self._recommendations = rec[[
'Firm', 'To Grade', 'From Grade', 'Action']].sort_index()
except Exception:
pass
self._get_info(proxy)
# get fundamentals
data = utils.get_json(ticker_url + '/financials', proxy, self.session)
# generic patterns
self._earnings = {"yearly": utils.empty_df(), "quarterly": utils.empty_df()}
self._cashflow = {"yearly": utils.empty_df(), "quarterly": utils.empty_df()}
self._balancesheet = {"yearly": utils.empty_df(), "quarterly": utils.empty_df()}
self._financials = {"yearly": utils.empty_df(), "quarterly": utils.empty_df()}
for key in (
(self._cashflow, 'cashflowStatement', 'cashflowStatements'),
(self._balancesheet, 'balanceSheet', 'balanceSheetStatements'),
@@ -511,11 +655,15 @@ class TickerBase():
# shares outstanding
try:
shares = _pd.DataFrame(data['annualBasicAverageShares'])
# keep only years with non None data
available_shares = [shares_data for shares_data in data['annualBasicAverageShares'] if shares_data]
shares = _pd.DataFrame(available_shares)
shares['Year'] = shares['asOfDate'].agg(lambda x: int(x[:4]))
shares.set_index('Year', inplace=True)
shares.drop(columns=['dataId', 'asOfDate', 'periodType', 'currencyCode'], inplace=True)
shares.rename(columns={'reportedValue': "BasicShares"}, inplace=True)
shares.drop(columns=['dataId', 'asOfDate',
'periodType', 'currencyCode'], inplace=True)
shares.rename(
columns={'reportedValue': "BasicShares"}, inplace=True)
self._shares = shares
except Exception:
pass
@@ -539,62 +687,26 @@ class TickerBase():
if isinstance(colval, dict):
dict_cols.append(colname)
for k, v in colval.items():
new_colname = colname + ' ' + utils.camel2title([k])[0]
new_colname = colname + ' ' + \
utils.camel2title([k])[0]
analysis.loc[idx, new_colname] = v
self._analysis = analysis[[c for c in analysis.columns if c not in dict_cols]]
self._analysis = analysis[[
c for c in analysis.columns if c not in dict_cols]]
except Exception:
pass
# Complementary key-statistics (currently fetching the important trailingPegRatio which is the value shown in the website)
res = {}
try:
my_headers = {'user-agent': 'curl/7.55.1', 'accept': 'application/json', 'content-type': 'application/json', 'referer': 'https://finance.yahoo.com/', 'cache-control': 'no-cache', 'connection': 'close'}
p = _re.compile(r'root\.App\.main = (.*);')
r = _requests.session().get('https://finance.yahoo.com/quote/{}/key-statistics?p={}'.format(self.ticker, self.ticker), headers=my_headers)
q_results = {}
my_qs_keys = ['pegRatio'] # QuoteSummaryStore
my_ts_keys = ['trailingPegRatio'] # , 'quarterlyPegRatio'] # QuoteTimeSeriesStore
# Complementary key-statistics
data = _json.loads(p.findall(r.text)[0])
key_stats = data['context']['dispatcher']['stores']['QuoteTimeSeriesStore']
q_results.setdefault(self.ticker, [])
for i in my_ts_keys:
# j=0
try:
# res = {i: key_stats['timeSeries'][i][1]['reportedValue']['raw']}
# We need to loop over multiple items, if they exist: 0,1,2,..
zzz = key_stats['timeSeries'][i]
for j in range(len(zzz)):
if key_stats['timeSeries'][i][j]:
res = {i: key_stats['timeSeries'][i][j]['reportedValue']['raw']}
q_results[self.ticker].append(res)
# print(res)
# q_results[ticker].append(res)
except:
q_results[ticker].append({i: np.nan})
res = {'Company': ticker}
q_results[ticker].append(res)
except Exception:
pass
if 'trailingPegRatio' in res:
self._info['trailingPegRatio'] = res['trailingPegRatio']
self._fundamentals = True
def get_recommendations(self, proxy=None, as_dict=False, *args, **kwargs):
self._get_fundamentals(proxy=proxy)
self._get_info(proxy)
data = self._recommendations
if as_dict:
return data.to_dict()
return data
def get_calendar(self, proxy=None, as_dict=False, *args, **kwargs):
self._get_fundamentals(proxy=proxy)
self._get_info(proxy)
data = self._calendar
if as_dict:
return data.to_dict()
@@ -624,14 +736,14 @@ class TickerBase():
return data
def get_info(self, proxy=None, as_dict=False, *args, **kwargs):
self._get_fundamentals(proxy=proxy)
self._get_info(proxy)
data = self._info
if as_dict:
return data.to_dict()
return data
def get_sustainability(self, proxy=None, as_dict=False, *args, **kwargs):
self._get_fundamentals(proxy=proxy)
self._get_info(proxy)
data = self._sustainability
if as_dict:
return data.to_dict()
@@ -729,6 +841,10 @@ class TickerBase():
self.get_info(proxy=proxy)
if "shortName" in self._info:
q = self._info['shortName']
if q is None:
err_msg = "Cannot map to ISIN code, symbol may be delisted"
print('- %s: %s' % (self.ticker, err_msg))
return None
url = 'https://markets.businessinsider.com/ajax/' \
'SearchController_Suggest?max_results=25&query=%s' \
@@ -781,3 +897,123 @@ class TickerBase():
# parse news
self._news = data.get("news", [])
return self._news
def get_earnings_dates(self, proxy=None):
if self._earnings_dates is not None:
return self._earnings_dates
# setup proxy in requests format
if proxy is not None:
if isinstance(proxy, dict) and "https" in proxy:
proxy = proxy["https"]
proxy = {"https": proxy}
page_size = 100 # YF caps at 100, don't go higher
page_offset = 0
dates = None
while True:
url = "{}/calendar/earnings?symbol={}&offset={}&size={}".format(
_ROOT_URL_, self.ticker, page_offset, page_size)
session = self.session or _requests
data = session.get(
url=url,
proxies=proxy,
headers=utils.user_agent_headers
).text
if "Will be right back" in data:
raise RuntimeError("*** YAHOO! FINANCE IS CURRENTLY DOWN! ***\n"
"Our engineers are working quickly to resolve "
"the issue. Thank you for your patience.")
try:
data = _pd.read_html(data)[0]
except ValueError:
if page_offset == 0:
# Should not fail on first page
if "Showing Earnings for:" in data:
# Actually YF was successful, problem is company doesn't have earnings history
dates = utils.empty_earnings_dates_df()
break
if dates is None:
dates = data
else:
dates = _pd.concat([dates, data], axis=0)
page_offset += page_size
if (dates is None) or dates.shape[0]==0:
err_msg = "No earnings dates found, symbol may be delisted"
print('- %s: %s' % (self.ticker, err_msg))
return None
dates = dates.reset_index(drop=True)
# Drop redundant columns
dates = dates.drop(["Symbol", "Company"], axis=1)
# Convert types
for cn in ["EPS Estimate", "Reported EPS", "Surprise(%)"]:
dates.loc[dates[cn] == '-', cn] = "NaN"
dates[cn] = dates[cn].astype(float)
# Convert % to range 0->1:
dates["Surprise(%)"] *= 0.01
# Parse earnings date string
cn = "Earnings Date"
# - remove AM/PM and timezone from date string
tzinfo = dates[cn].str.extract('([AP]M[a-zA-Z]*)$')
dates[cn] = dates[cn].replace(' [AP]M[a-zA-Z]*$', '', regex=True)
# - split AM/PM from timezone
tzinfo = tzinfo[0].str.extract('([AP]M)([a-zA-Z]*)', expand=True)
tzinfo.columns = ["AM/PM", "TZ"]
# - combine and parse
dates[cn] = dates[cn] + ' ' + tzinfo["AM/PM"]
dates[cn] = _pd.to_datetime(dates[cn], format="%b %d, %Y, %I %p")
# - instead of attempting decoding of ambiguous timezone abbreviation, just use 'info':
dates[cn] = dates[cn].dt.tz_localize(
tz=self.info["exchangeTimezoneName"])
dates = dates.set_index("Earnings Date")
self._earnings_dates = dates
return dates
def get_earnings_history(self, proxy=None):
if self._earnings_history:
return self._earnings_history
# setup proxy in requests format
if proxy is not None:
if isinstance(proxy, dict) and "https" in proxy:
proxy = proxy["https"]
proxy = {"https": proxy}
url = "{}/calendar/earnings?symbol={}".format(_ROOT_URL_, self.ticker)
session = self.session or _requests
data = session.get(
url=url,
proxies=proxy,
headers=utils.user_agent_headers
).text
if "Will be right back" in data:
raise RuntimeError("*** YAHOO! FINANCE IS CURRENTLY DOWN! ***\n"
"Our engineers are working quickly to resolve "
"the issue. Thank you for your patience.")
try:
# read_html returns a list of pandas Dataframes of all the tables in `data`
data = _pd.read_html(data)[0]
data.replace("-", _np.nan, inplace=True)
data['EPS Estimate'] = _pd.to_numeric(data['EPS Estimate'])
data['Reported EPS'] = _pd.to_numeric(data['Reported EPS'])
self._earnings_history = data
# if no tables are found a ValueError is thrown
except ValueError:
print("Could not find data for {}.".format(self.ticker))
return
return data

View File

@@ -29,8 +29,8 @@ from . import Ticker, utils
from . import shared
def download(tickers, start=None, end=None, actions=False, threads=True,
group_by='column', auto_adjust=False, back_adjust=False,
def download(tickers, start=None, end=None, actions=False, threads=True, ignore_tz=True,
group_by='column', auto_adjust=False, back_adjust=False, keepna=False,
progress=True, period="max", show_errors=True, interval="1d", prepost=False,
proxy=None, rounding=False, timeout=None, **kwargs):
"""Download yahoo tickers
@@ -56,16 +56,22 @@ def download(tickers, start=None, end=None, actions=False, threads=True,
Default is False
auto_adjust: bool
Adjust all OHLC automatically? 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 is True
proxy: str
Optional. Proxy server URL scheme. Default is None
rounding: bool
Optional. Round values to 2 decimal places?
show_errors: bool
Optional. Doesn't print errors if True
Optional. Doesn't print errors if False
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)
@@ -105,7 +111,7 @@ def download(tickers, start=None, end=None, actions=False, threads=True,
_download_one_threaded(ticker, period=period, interval=interval,
start=start, end=end, prepost=prepost,
actions=actions, auto_adjust=auto_adjust,
back_adjust=back_adjust,
back_adjust=back_adjust, keepna=keepna,
progress=(progress and i > 0), proxy=proxy,
rounding=rounding, timeout=timeout)
while len(shared._DFS) < len(tickers):
@@ -117,7 +123,7 @@ def download(tickers, start=None, end=None, actions=False, threads=True,
data = _download_one(ticker, period=period, interval=interval,
start=start, end=end, prepost=prepost,
actions=actions, auto_adjust=auto_adjust,
back_adjust=back_adjust, proxy=proxy,
back_adjust=back_adjust, keepna=keepna, proxy=proxy,
rounding=rounding, timeout=timeout)
shared._DFS[ticker.upper()] = data
if progress:
@@ -133,16 +139,21 @@ def download(tickers, start=None, end=None, actions=False, threads=True,
print("\n".join(['- %s: %s' %
v for v in list(shared._ERRORS.items())]))
if ignore_tz:
for tkr in shared._DFS.keys():
if (shared._DFS[tkr] is not None) and (shared._DFS[tkr].shape[0]>0):
shared._DFS[tkr].index = shared._DFS[tkr].index.tz_localize(None)
if len(tickers) == 1:
ticker = tickers[0]
return shared._DFS[shared._ISINS.get(ticker, ticker)]
try:
data = _pd.concat(shared._DFS.values(), axis=1,
data = _pd.concat(shared._DFS.values(), axis=1, sort=True,
keys=shared._DFS.keys())
except Exception:
_realign_dfs()
data = _pd.concat(shared._DFS.values(), axis=1,
data = _pd.concat(shared._DFS.values(), axis=1, sort=True,
keys=shared._DFS.keys())
# switch names back to isins if applicable
@@ -183,11 +194,11 @@ def _download_one_threaded(ticker, start=None, end=None,
auto_adjust=False, back_adjust=False,
actions=False, progress=True, period="max",
interval="1d", prepost=False, proxy=None,
rounding=False, timeout=None):
keepna=False, rounding=False, timeout=None):
data = _download_one(ticker, start, end, auto_adjust, back_adjust,
actions, period, interval, prepost, proxy, rounding,
timeout)
keepna, timeout)
shared._DFS[ticker.upper()] = data
if progress:
shared._PROGRESS_BAR.animate()
@@ -197,11 +208,11 @@ def _download_one(ticker, start=None, end=None,
auto_adjust=False, back_adjust=False,
actions=False, period="max", interval="1d",
prepost=False, proxy=None, rounding=False,
timeout=None):
keepna=False, timeout=None):
return Ticker(ticker).history(period=period, interval=interval,
start=start, end=end, prepost=prepost,
actions=actions, auto_adjust=auto_adjust,
back_adjust=back_adjust, proxy=proxy,
rounding=rounding, many=True,
rounding=rounding, keepna=keepna, many=True,
timeout=timeout)

View File

@@ -211,3 +211,11 @@ class Ticker(TickerBase):
@property
def analysis(self):
return self.get_analysis()
@property
def earnings_history(self):
return self.get_earnings_history()
@property
def earnings_dates(self):
return self.get_earnings_dates()

View File

@@ -30,14 +30,14 @@ class Tickers():
def __repr__(self):
return 'yfinance.Tickers object <%s>' % ",".join(self.symbols)
def __init__(self, tickers):
def __init__(self, tickers, session=None):
tickers = tickers if isinstance(
tickers, list) else tickers.replace(',', ' ').split()
self.symbols = [ticker.upper() for ticker in tickers]
ticker_objects = {}
for ticker in self.symbols:
ticker_objects[ticker] = Ticker(ticker)
ticker_objects[ticker] = Ticker(ticker, session=session)
self.tickers = ticker_objects
# self.tickers = _namedtuple(
@@ -51,11 +51,11 @@ class Tickers():
timeout=None, **kwargs):
return self.download(
period, interval,
start, end, prepost,
actions, auto_adjust, proxy,
threads, group_by, progress,
timeout, **kwargs)
period, interval,
start, end, prepost,
actions, auto_adjust, proxy,
threads, group_by, progress,
timeout, **kwargs)
def download(self, period="1mo", interval="1d",
start=None, end=None, prepost=False,
@@ -85,3 +85,12 @@ class Tickers():
data.sort_index(level=0, axis=1, inplace=True)
return data
def news(self):
collection = {}
for ticker in self.symbols:
collection[ticker] = []
items = Ticker(ticker).news
for item in items:
collection[ticker].append(item)
return collection

View File

@@ -21,11 +21,18 @@
from __future__ import print_function
import datetime as _datetime
import pytz as _tz
import requests as _requests
import re as _re
import pandas as _pd
import numpy as _np
import sys as _sys
import os as _os
import appdirs as _ad
from threading import Lock
mutex = Lock()
try:
import ujson as _json
@@ -33,7 +40,8 @@ except ImportError:
import json as _json
user_agent_headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36'}
user_agent_headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36'}
def is_isin(string):
@@ -88,6 +96,13 @@ def empty_df(index=[]):
return empty
def empty_earnings_dates_df():
empty = _pd.DataFrame(
columns=["Symbol", "Company", "Earnings Date",
"EPS Estimate", "Reported EPS", "Surprise(%)"])
return empty
def get_html(url, proxy=None, session=None):
session = session or _requests
html = session.get(url=url, proxies=proxy, headers=user_agent_headers).text
@@ -109,8 +124,9 @@ def get_json(url, proxy=None, session=None):
'context']['dispatcher']['stores']['QuoteSummaryStore']
# add data about Shares Outstanding for companies' tickers if they are available
try:
data['annualBasicAverageShares'] = _json.loads(json_str)[
'context']['dispatcher']['stores']['QuoteTimeSeriesStore']['timeSeries']['annualBasicAverageShares']
data['annualBasicAverageShares'] = _json.loads(
json_str)['context']['dispatcher']['stores'][
'QuoteTimeSeriesStore']['timeSeries']['annualBasicAverageShares']
except Exception:
pass
@@ -126,6 +142,23 @@ def camel2title(o):
return [_re.sub("([a-z])([A-Z])", r"\g<1> \g<2>", i).title() for i in o]
def _parse_user_dt(dt, exchange_tz):
if isinstance(dt, int):
## Should already be epoch, test with conversion:
_datetime.datetime.fromtimestamp(dt)
else:
# Convert str/date -> datetime, set tzinfo=exchange, get timestamp:
if isinstance(dt, str):
dt = _datetime.datetime.strptime(str(dt), '%Y-%m-%d')
if isinstance(dt, _datetime.date) and not isinstance(dt, _datetime.datetime):
dt = _datetime.datetime.combine(dt, _datetime.time(0))
if isinstance(dt, _datetime.datetime) and dt.tzinfo is None:
# Assume user is referring to exchange's timezone
dt = _tz.timezone(exchange_tz).localize(dt)
dt = int(dt.timestamp())
return dt
def auto_adjust(data):
df = data.copy()
ratio = df["Close"] / df["Adj Close"]
@@ -167,7 +200,7 @@ def back_adjust(data):
return df[["Open", "High", "Low", "Close", "Volume"]]
def parse_quotes(data, tz=None):
def parse_quotes(data):
timestamps = data["timestamp"]
ohlc = data["indicators"]["quote"][0]
volumes = ohlc["volume"]
@@ -190,15 +223,14 @@ def parse_quotes(data, tz=None):
quotes.index = _pd.to_datetime(timestamps, unit="s")
quotes.sort_index(inplace=True)
if tz is not None:
quotes.index = quotes.index.tz_localize(tz)
return quotes
def parse_actions(data, tz=None):
dividends = _pd.DataFrame(columns=["Dividends"])
splits = _pd.DataFrame(columns=["Stock Splits"])
def parse_actions(data):
dividends = _pd.DataFrame(
columns=["Dividends"], index=_pd.DatetimeIndex([]))
splits = _pd.DataFrame(
columns=["Stock Splits"], index=_pd.DatetimeIndex([]))
if "events" in data:
if "dividends" in data["events"]:
@@ -207,8 +239,6 @@ def parse_actions(data, tz=None):
dividends.set_index("date", inplace=True)
dividends.index = _pd.to_datetime(dividends.index, unit="s")
dividends.sort_index(inplace=True)
if tz is not None:
dividends.index = dividends.index.tz_localize(tz)
dividends.columns = ["Dividends"]
@@ -218,8 +248,6 @@ def parse_actions(data, tz=None):
splits.set_index("date", inplace=True)
splits.index = _pd.to_datetime(splits.index, unit="s")
splits.sort_index(inplace=True)
if tz is not None:
splits.index = splits.index.tz_localize(tz)
splits["Stock Splits"] = splits["numerator"] / \
splits["denominator"]
splits = splits["Stock Splits"]
@@ -227,6 +255,19 @@ def parse_actions(data, tz=None):
return dividends, splits
def fix_Yahoo_dst_issue(df, interval):
if interval in ["1d","1w","1wk"]:
# These intervals should start at time 00:00. But for some combinations of date and timezone,
# Yahoo has time off by few hours (e.g. Brazil 23:00 around Jan-2022). Suspect DST problem.
# The clue is (a) minutes=0 and (b) hour near 0.
# Obviously Yahoo meant 00:00, so ensure this doesn't affect date conversion:
f_pre_midnight = (df.index.minute == 0) & (df.index.hour.isin([22,23]))
dst_error_hours = _np.array([0]*df.shape[0])
dst_error_hours[f_pre_midnight] = 24-df.index[f_pre_midnight].hour
df.index += _pd.TimedeltaIndex(dst_error_hours, 'h')
return df
class ProgressBar:
def __init__(self, iterations, text='completed'):
self.text = text
@@ -275,3 +316,55 @@ class ProgressBar:
def __str__(self):
return str(self.prog_bar)
# Simple file cache of ticker->timezone:
_cache_dp = None
def get_cache_dirpath():
if _cache_dp is None:
dp = _os.path.join(_ad.user_cache_dir(), "py-yfinance")
else:
dp = _os.path.join(_cache_dp, "py-yfinance")
return dp
def set_tz_cache_location(dp):
global _cache_dp
_cache_dp = dp
def cache_lookup_tkr_tz(tkr):
fp = _os.path.join(get_cache_dirpath(), "tkr-tz.csv")
if not _os.path.isfile(fp):
return None
mutex.acquire()
df = _pd.read_csv(fp, index_col="Ticker", on_bad_lines="skip")
mutex.release()
if tkr in df.index:
return df.loc[tkr,"Tz"]
else:
return None
def cache_store_tkr_tz(tkr,tz):
dp = get_cache_dirpath()
fp = _os.path.join(dp, "tkr-tz.csv")
mutex.acquire()
if not _os.path.isdir(dp):
_os.makedirs(dp)
if (not _os.path.isfile(fp)) and (tz is not None):
df = _pd.DataFrame({"Tz":[tz]}, index=[tkr])
df.index.name = "Ticker"
df.to_csv(fp)
else:
df = _pd.read_csv(fp, index_col="Ticker", on_bad_lines="skip")
if tz is None:
# Delete if in cache:
if tkr in df.index:
df.drop(tkr).to_csv(fp)
else:
if tkr in df.index:
raise Exception("Tkr {} tz already in cache".format(tkr))
df.loc[tkr,"Tz"] = tz
df.to_csv(fp)
mutex.release()

View File

@@ -1 +1 @@
version = "0.1.68"
version = "0.1.86"