Compare commits
99 Commits
fix/screen
...
0.2.66
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
173ab2d811 | ||
|
|
8cca819fd6 | ||
|
|
bc67252d4b | ||
|
|
f2d617b948 | ||
|
|
4e4b0af3ef | ||
|
|
04ba66083e | ||
|
|
bd327694c5 | ||
|
|
99b2db69e0 | ||
|
|
5a6efdf773 | ||
|
|
4ff80a57d4 | ||
|
|
441ae8d677 | ||
|
|
8b71e6cc05 | ||
|
|
bc40e84461 | ||
|
|
ead4ed7ee7 | ||
|
|
2e0ad642c1 | ||
|
|
54b37a403c | ||
|
|
a04a4ea475 | ||
|
|
9f0d3fd12d | ||
|
|
3596924fe2 | ||
|
|
4259760bea | ||
|
|
9a6c82aea6 | ||
|
|
bdb2fecf1e | ||
|
|
0b0b60e535 | ||
|
|
733091ad6c | ||
|
|
ea4dc2d22a | ||
|
|
b29cc269c6 | ||
|
|
8db46f2b12 | ||
|
|
f8e75b3aaf | ||
|
|
8f04f91e2b | ||
|
|
4a1e1a9fb2 | ||
|
|
a05a759852 | ||
|
|
9b7f53689b | ||
|
|
31109b2a2a | ||
|
|
8e88aec77d | ||
|
|
068e6b04db | ||
|
|
90575a029f | ||
|
|
5dbc2950e8 | ||
|
|
4707bc035e | ||
|
|
81011a7a75 | ||
|
|
14ec3df9a4 | ||
|
|
1056487183 | ||
|
|
b7a425d683 | ||
|
|
a61e0a07b5 | ||
|
|
e2150daf18 | ||
|
|
69dbf74292 | ||
|
|
b5c2160837 | ||
|
|
b8ab067caa | ||
|
|
c6429482ab | ||
|
|
4f9b6d6e9f | ||
|
|
5812aa6a69 | ||
|
|
ea810b4b35 | ||
|
|
53bce46929 | ||
|
|
0eb11c56b0 | ||
|
|
2981893f30 | ||
|
|
3250386136 | ||
|
|
aa606642c0 | ||
|
|
cc0b03efd1 | ||
|
|
39dd87080d | ||
|
|
72a2fd8955 | ||
|
|
dd62ce510e | ||
|
|
61ceb2b1a8 | ||
|
|
5d7a298239 | ||
|
|
0192d2e194 | ||
|
|
8c6eb1afeb | ||
|
|
ef60663bc2 | ||
|
|
d15cf378a1 | ||
|
|
a9282e5739 | ||
|
|
a506838c3c | ||
|
|
f716eec5fe | ||
|
|
e769570f33 | ||
|
|
3bc6bacf56 | ||
|
|
7db82f3496 | ||
|
|
4ac5cd87b3 | ||
|
|
4a91008c09 | ||
|
|
de0760eec8 | ||
|
|
55bc1bdced | ||
|
|
b509dc9551 | ||
|
|
bb6ebb4b84 | ||
|
|
d08afa21fc | ||
|
|
5bd805b3f6 | ||
|
|
54c6ac3ed7 | ||
|
|
68b7c16162 | ||
|
|
78ad990371 | ||
|
|
81d8737a25 | ||
|
|
890026c862 | ||
|
|
22e4219ec7 | ||
|
|
ac9184bf18 | ||
|
|
745d554aae | ||
|
|
ec5548bd85 | ||
|
|
63b56a799a | ||
|
|
d1dde1814d | ||
|
|
4c23d339bc | ||
|
|
3b36df048b | ||
|
|
6f0f408880 | ||
|
|
9158d3c119 | ||
|
|
2608f3660c | ||
|
|
e614e6b8ff | ||
|
|
4c609b42c6 | ||
|
|
b8ae8f317f |
12
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
12
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -18,7 +18,15 @@ body:
|
||||
|
||||
Are you spelling symbol *exactly* same as Yahoo?
|
||||
|
||||
Then visit `finance.yahoo.com` and confirm they have the data you want. Maybe your symbol was delisted, or your expectations of `yfinance` are wrong.
|
||||
Then visit `finance.yahoo.com` and confirm they have the data you want. Maybe your symbol was delisted.
|
||||
|
||||
### Data is wrong
|
||||
|
||||
**yfinance is not affiliated with Yahoo**. If Yahoo Finance website data is bad, tell Yahoo: https://help.yahoo.com/kb/finance-for-web/report-problems-feedback-ideas-yahoo-finance-sln28397.html.
|
||||
|
||||
But if yfinance is creating error during processing, then finish this form.
|
||||
|
||||
For price errors, try price repair: https://ranaroussi.github.io/yfinance/advanced/price_repair.html
|
||||
|
||||
### Are you spamming Yahoo?
|
||||
|
||||
@@ -63,7 +71,7 @@ body:
|
||||
id: bad-data-proof
|
||||
attributes:
|
||||
label: "Bad data proof"
|
||||
description: "If `yfinance` returning bad data, show proof of good data here. Best proof is screenshot of finance.yahoo.com"
|
||||
description: "If yfinance returning bad data, show proof of good data on Yahoo Finance website here."
|
||||
validations:
|
||||
required: false
|
||||
|
||||
|
||||
36
.github/workflows/auto_close_default_issues.yml
vendored
Normal file
36
.github/workflows/auto_close_default_issues.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: Auto-close issues using default template
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
check-template:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check if issue uses custom template
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const issue = context.payload.issue;
|
||||
const body = issue.body || '';
|
||||
|
||||
// Check for specific fields from your custom form
|
||||
// Adjust these patterns based on your form structure
|
||||
const hasCustomFields = body.includes('### Describe bug') ||
|
||||
body.includes('### Simple code that reproduces');
|
||||
|
||||
if (!hasCustomFields) {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body: 'This issue appears to use the default template. Stop that. Use our custom bug report form.'
|
||||
});
|
||||
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
state: 'closed'
|
||||
});
|
||||
}
|
||||
3
.github/workflows/deploy_doc.yml
vendored
3
.github/workflows/deploy_doc.yml
vendored
@@ -3,7 +3,8 @@ name: Build and Deploy Sphinx Docs
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev-documented
|
||||
- main
|
||||
# - dev-documented
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
6
.github/workflows/ruff.yml
vendored
6
.github/workflows/ruff.yml
vendored
@@ -9,5 +9,7 @@ jobs:
|
||||
ruff:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: chartboost/ruff-action@v1
|
||||
- uses: actions/checkout@v4
|
||||
- uses: astral-sh/ruff-action@v3
|
||||
with:
|
||||
args: check . --exclude yfinance/pricing_pb2.py
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -9,7 +9,6 @@ yfinance.egg-info
|
||||
build/
|
||||
*.html
|
||||
*.css
|
||||
*.png
|
||||
test.ipynb
|
||||
|
||||
# Environments
|
||||
@@ -24,4 +23,4 @@ ENV/
|
||||
/doc/_build/
|
||||
/doc/source/reference/api
|
||||
!yfinance.css
|
||||
!/doc/source/development/assets/branches.png
|
||||
!/doc/source/development/assets/branches.png
|
||||
|
||||
@@ -1,6 +1,63 @@
|
||||
Change Log
|
||||
===========
|
||||
|
||||
0.2.66
|
||||
------
|
||||
Screener: add Swiss exchange and industry field
|
||||
Support MIC #2579
|
||||
Fixes:
|
||||
- parse epoch dt #2573
|
||||
- earnings_dates #2591
|
||||
- merge intraday prices with divs/splits #2595
|
||||
- exceptions missing detail #2599
|
||||
|
||||
0.2.65
|
||||
------
|
||||
Financials: ensure dtype float #2563
|
||||
Prices: fix handling arguments start/end/period #2561 #2562
|
||||
Price repair: when changing FX, update metadata #2558
|
||||
|
||||
0.2.64
|
||||
------
|
||||
Prices:
|
||||
- handle dividends with FX, convert if repair=True #2549
|
||||
- fix 'period' arg when start or end set #2550
|
||||
earnings_dates: handle 'Event Type' properly #2555
|
||||
|
||||
0.2.63
|
||||
------
|
||||
Fix download(ISIN) # 2531
|
||||
|
||||
0.2.62
|
||||
------
|
||||
Fix prices 'period=max' sometimes failing # 2509
|
||||
ISIN cache #2516
|
||||
Proxy:
|
||||
- fix false 'proxy deprecated' messages
|
||||
- fix ISIN + proxy #2514
|
||||
- replace print_once with warnings #2523
|
||||
Error handling:
|
||||
- detect rate-limit during crumb fetch #2491
|
||||
- replace requests.HTTPError with curl_cffi
|
||||
|
||||
0.2.61
|
||||
------
|
||||
Fix ALL type hints in websocket #2493
|
||||
|
||||
0.2.60
|
||||
------
|
||||
Fix cookie reuse, and handle DNS blocking fc.yahoo.com #2483
|
||||
Fixes for websocket:
|
||||
- relax protobuf version #2485
|
||||
- increase websockets version #2485
|
||||
- fix type hints #2488
|
||||
Fix predefined screen offset #2440
|
||||
|
||||
0.2.59
|
||||
------
|
||||
Fix the fix for rate-limit #2452
|
||||
Feature: live price data websocket #2201
|
||||
|
||||
0.2.58
|
||||
------
|
||||
Fix false rate-limit problem #2430
|
||||
|
||||
158
CONTRIBUTING.md
158
CONTRIBUTING.md
@@ -1,90 +1,23 @@
|
||||
# Contributing
|
||||
|
||||
## Changes
|
||||
|
||||
The list of changes can be found in the [Changelog](https://github.com/ranaroussi/yfinance/blob/main/CHANGELOG.rst)
|
||||
|
||||
## Running a branch
|
||||
|
||||
### With PIP
|
||||
|
||||
```bash
|
||||
pip install git+https://github.com/ranaroussi/yfinance.git@BRANCH
|
||||
```
|
||||
|
||||
E.g.:
|
||||
|
||||
```bash
|
||||
pip install git+https://github.com/ranaroussi/yfinance.git@feature/something
|
||||
```
|
||||
|
||||
### With Git
|
||||
|
||||
1: Download from GitHub:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/ranaroussi/yfinance.git
|
||||
```
|
||||
|
||||
Or if a specific branch:
|
||||
|
||||
```bash
|
||||
git clone -b <BRANCH NAME> https://github.com/ranaroussi/yfinance.git
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Only do the next part if you are installing globally
|
||||
>
|
||||
> If you are installing for 1 specific project, then you can skip this step
|
||||
> and just `git clone` in the project directory
|
||||
|
||||
2: Add download location to Python search path
|
||||
|
||||
Two different ways, choose one:
|
||||
|
||||
`. Add path to PYTHONPATH environment variable
|
||||
1. Add to top of Python file:
|
||||
```python
|
||||
import sys
|
||||
sys.path.insert(0, "path/to/downloaded/yfinance")
|
||||
```
|
||||
|
||||
3: Verify
|
||||
|
||||
```python
|
||||
import yfinance
|
||||
print(yfinance)
|
||||
```
|
||||
|
||||
Output should be:
|
||||
|
||||
`<module 'yfinance' from 'path/to/downloaded/yfinance/yfinance/__init__.py'>`
|
||||
|
||||
If output looks like this then you did step 2 wrong
|
||||
|
||||
`<module 'yfinance' from '.../lib/python3.10/site-packages/yfinance/__init__.py'>`
|
||||
yfinance relies on the community to investigate bugs and contribute code.
|
||||
|
||||
This is a quick short guide, full guide at https://ranaroussi.github.io/yfinance/development/index.html
|
||||
|
||||
## Branches
|
||||
|
||||
To support rapid development without breaking stable versions, this project uses a two-layer branch model:  ([inspiration](https://miro.medium.com/max/700/1*2YagIpX6LuauC3ASpwHekg.png))
|
||||
YFinance uses a two-layer branch model:
|
||||
|
||||
* **dev** : new features & some bug-fixes merged here, tested together, conflicts fixed, etc. Once stable, can merge to ...
|
||||
* **dev**: new features & most bug-fixes merged here, tested together, conflicts fixed, etc.
|
||||
* **main**: stable branch where PIP releases are created.
|
||||
|
||||
* **main**: stable branch. Where PIP releases are created
|
||||
|
||||
|
||||
Most of the time you want to target the **dev** branch, but default is **main** so remember to switch to **dev**.
|
||||
|
||||
**Exception:** can straight merge into **main** if:
|
||||
|
||||
* yfinance is massively broken
|
||||
|
||||
* or part of yfinance is broken and the fix is simple and clearly doesn't affect anything else
|
||||
|
||||
* or not editing part of the code (e.g. editing this file)
|
||||
## Running a branch
|
||||
|
||||
```bash
|
||||
pip install git+ranaroussi/yfinance.git@dev # <- dev branch
|
||||
```
|
||||
|
||||
https://ranaroussi.github.io/yfinance/development/running.html
|
||||
|
||||
### I'm a GitHub newbie, how do I contribute code?
|
||||
|
||||
@@ -92,67 +25,36 @@ Most of the time you want to target the **dev** branch, but default is **main**
|
||||
|
||||
2. Implement your change in your fork, ideally in a specific branch
|
||||
|
||||
3. Create a Pull Request, from your fork to this project. If addressing an Issue, link to it
|
||||
3. Create a [Pull Request](https://github.com/ranaroussi/yfinance/pulls), from your fork to this project. If addressing an Issue, link to it
|
||||
|
||||
|
||||
### [How to download & run a GitHub version of yfinance](https://github.com/ranaroussi/yfinance/discussions/1080)
|
||||
https://ranaroussi.github.io/yfinance/development/code.html
|
||||
|
||||
## Documentation website
|
||||
|
||||
The new docs website [ranaroussi.github.io/yfinance/index.html](https://ranaroussi.github.io/yfinance/index.html) is generated automatically from code. When you fetch a branch or PR, probably it also has changes to docs - to generate and view this:
|
||||
The new docs website is generated automatically from code. https://ranaroussi.github.io/yfinance/index.html
|
||||
|
||||
```bash
|
||||
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
|
||||
sphinx-build -b html doc/source doc/_build/html
|
||||
python -m http.server -d ./doc/_build/html
|
||||
# open "localhost:8000" in browser
|
||||
```
|
||||
Remember to updates docs when you change code, and check docs locally.
|
||||
|
||||
https://ranaroussi.github.io/yfinance/development/documentation.html
|
||||
|
||||
## Git tricks
|
||||
|
||||
Help keep the Git commit history and [network graph](https://github.com/ranaroussi/yfinance/network) compact:
|
||||
|
||||
* got a long descriptive commit message? `git commit -m "short sentence summary" -m "full commit message"`
|
||||
|
||||
* combine multiple commits into 1 with `git squash`
|
||||
|
||||
* `git rebase` is your friend: change base branch, or "merge in" updates
|
||||
|
||||
https://ranaroussi.github.io/yfinance/development/code.html#git-stuff
|
||||
|
||||
## Unit tests
|
||||
|
||||
Tests have been written using the built-in Python module `unittest`. Examples:
|
||||
|
||||
* Run all price tests: `python -m unittest tests.test_prices`
|
||||
|
||||
* Run sub-set of price tests: `python -m unittest tests.test_prices.TestPriceRepair`
|
||||
|
||||
* Run a specific test: `python -m unittest tests.test_prices.TestPriceRepair.test_ticker_missing`
|
||||
|
||||
* Run all tests: `python -m unittest discover -s tests`
|
||||
|
||||
> [!NOTE]
|
||||
> The tests are currently failing already
|
||||
>
|
||||
> Standard result:
|
||||
>
|
||||
> **Failures:** 11
|
||||
>
|
||||
> **Errors:** 93
|
||||
>
|
||||
> **Skipped:** 1
|
||||
https://ranaroussi.github.io/yfinance/development/testing.html
|
||||
|
||||
## Git stuff
|
||||
### commits
|
||||
|
||||
To keep the Git commit history and [network graph](https://github.com/ranaroussi/yfinance/network) compact please follow these two rules:
|
||||
|
||||
* For long commit messages use this: `git commit -m "short sentence summary" -m "full commit message"`
|
||||
|
||||
* `squash` tiny/negligible commits back with meaningful commits, or to combine successive related commits. [Guide](https://docs.gitlab.com/ee/topics/git/git_rebase.html#interactive-rebase) but basically it's:
|
||||
|
||||
|
||||
```bash
|
||||
git rebase -i HEAD~2
|
||||
git push --force-with-lease origin <branch-name>
|
||||
```
|
||||
|
||||
### rebase
|
||||
|
||||
You might be asked to move your branch from `main` to `dev`. Make sure you have pulled **all** relevant branches then run:
|
||||
|
||||
```bash
|
||||
git checkout <your branch>
|
||||
git rebase --onto dev main <branch-name>
|
||||
git push --force-with-lease origin <branch-name>
|
||||
```
|
||||
> See the [Developer Guide](https://ranaroussi.github.io/yfinance/development/contributing.html#GIT-STUFF) for more information.
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
<a target="new" href="https://pypi.python.org/pypi/yfinance"><img border=0 src="https://img.shields.io/badge/python-2.7,%203.6+-blue.svg?style=flat" alt="Python version"></a>
|
||||
<a target="new" href="https://pypi.python.org/pypi/yfinance"><img border=0 src="https://img.shields.io/pypi/v/yfinance.svg?maxAge=60%" alt="PyPi version"></a>
|
||||
<a target="new" href="https://pypi.python.org/pypi/yfinance"><img border=0 src="https://img.shields.io/pypi/status/yfinance.svg?maxAge=60" alt="PyPi status"></a>
|
||||
<a target="new" href="https://pypi.python.org/pypi/yfinance"><img border=0 src="https://img.shields.io/pypi/dm/yfinance.svg?maxAge=2592000&label=installs&color=%2327B1FF" alt="PyPi downloads"></a>
|
||||
<a target="new" href="https://pypi.python.org/pypi/yfinance"><img border=0 src="https://img.shields.io/pypi/dm/yfinance.svg?maxAge=86400&label=installs&color=%2327B1FF" alt="PyPi downloads"></a>
|
||||
<a target="new" href="https://github.com/ranaroussi/yfinance"><img border=0 src="https://img.shields.io/github/stars/ranaroussi/yfinance.svg?style=social&label=Star&maxAge=60" alt="Star this repo"></a>
|
||||
<a target="new" href="https://x.com/intent/follow?screen_name=aroussi"><img border=0 src="https://img.shields.io/twitter/follow/aroussi.svg?style=social&label=Follow&maxAge=60" alt="Follow me on twitter"></a>
|
||||
|
||||
|
||||
<a href="https://trendshift.io/repositories/4578" target="_blank"><img src="https://trendshift.io/api/badge/repositories/4578" alt="ranaroussi%2Fyfinance | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
|
||||
**yfinance** offers a Pythonic way to fetch financial & market data from [Yahoo!Ⓡ finance](https://finance.yahoo.com).
|
||||
|
||||
@@ -53,6 +53,8 @@ Install `yfinance` from PYPI using `pip`:
|
||||
$ pip install yfinance
|
||||
```
|
||||
|
||||
### [yfinance relies on the community to investigate bugs and contribute code. Here's how you can help.](CONTRIBUTING.md)
|
||||
|
||||
---
|
||||
|
||||

|
||||
|
||||
BIN
doc/source/_static/images/repair-prices-100x.png
Normal file
BIN
doc/source/_static/images/repair-prices-100x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
BIN
doc/source/_static/images/repair-prices-missing-div-adjust.png
Normal file
BIN
doc/source/_static/images/repair-prices-missing-div-adjust.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
BIN
doc/source/_static/images/repair-prices-missing-row.png
Normal file
BIN
doc/source/_static/images/repair-prices-missing-row.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
BIN
doc/source/_static/images/repair-prices-missing-split-adjust.png
Normal file
BIN
doc/source/_static/images/repair-prices-missing-split-adjust.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
BIN
doc/source/_static/images/repair-prices-missing-volume-daily.png
Normal file
BIN
doc/source/_static/images/repair-prices-missing-volume-daily.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -5,6 +5,7 @@
|
||||
.. currentmodule:: {{ module }}
|
||||
|
||||
.. autoclass:: {{ objname }}
|
||||
:exclude-members: {% for item in attributes %}{{ item }}{% if not loop.last %}, {% endif %}{% endfor %}
|
||||
|
||||
{% block attributes %}
|
||||
{% if attributes %}
|
||||
|
||||
@@ -1,47 +1,6 @@
|
||||
Caching
|
||||
=======
|
||||
|
||||
Smarter Scraping
|
||||
----------------
|
||||
|
||||
Install the `nospam` package to cache API calls and reduce spam to Yahoo:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip install yfinance[nospam]
|
||||
|
||||
To use a custom `requests` session, pass a `session=` argument to
|
||||
the Ticker constructor. This allows for caching calls to the API as well as a custom way to modify requests via the `User-agent` header.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import requests_cache
|
||||
session = requests_cache.CachedSession('yfinance.cache')
|
||||
session.headers['User-agent'] = 'my-program/1.0'
|
||||
ticker = yf.Ticker('MSFT', session=session)
|
||||
|
||||
# The scraped response will be stored in the cache
|
||||
ticker.actions
|
||||
|
||||
|
||||
Combine `requests_cache` with rate-limiting to avoid triggering Yahoo's rate-limiter/blocker that can corrupt data.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from requests import Session
|
||||
from requests_cache import CacheMixin, SQLiteCache
|
||||
from requests_ratelimiter import LimiterMixin, MemoryQueueBucket
|
||||
from pyrate_limiter import Duration, RequestRate, Limiter
|
||||
class CachedLimiterSession(CacheMixin, LimiterMixin, Session):
|
||||
pass
|
||||
|
||||
session = CachedLimiterSession(
|
||||
limiter=Limiter(RequestRate(2, Duration.SECOND*5)), # max 2 requests per 5 seconds
|
||||
bucket_class=MemoryQueueBucket,
|
||||
backend=SQLiteCache("yfinance.cache"),
|
||||
)
|
||||
|
||||
|
||||
Persistent Cache
|
||||
----------------
|
||||
|
||||
|
||||
@@ -8,4 +8,5 @@ Advanced
|
||||
logging
|
||||
config
|
||||
caching
|
||||
multi_level_columns
|
||||
multi_level_columns
|
||||
price_repair
|
||||
277
doc/source/advanced/price_repair.rst
Normal file
277
doc/source/advanced/price_repair.rst
Normal file
@@ -0,0 +1,277 @@
|
||||
************
|
||||
Price Repair
|
||||
************
|
||||
|
||||
The new argument ``repair=True`` in ``history()`` and ``download()`` will attempt to fix a variety of price errors caused by Yahoo. Only US market data appears perfect, I guess Yahoo doesn't care much about rest of world?
|
||||
|
||||
The returned table will have a new column ``Repaired?`` that specifies if row was repaired.
|
||||
|
||||
Price repair
|
||||
============
|
||||
|
||||
Missing dividend adjustment
|
||||
---------------------------
|
||||
|
||||
If dividend in data but preceding ``Adj Close`` = ``Close``, then manually apply dividend-adjustment to ``Adj Close``.
|
||||
Note: ``Repaired?`` is NOT set to ``True`` because fix only changes ``Adj Close``
|
||||
|
||||
.. figure:: /_static/images/repair-prices-missing-div-adjust.png
|
||||
:alt: 8TRA.DE: repair missing dividend adjustment
|
||||
:width: 80%
|
||||
:align: left
|
||||
|
||||
8TRA.DE
|
||||
|
||||
.. container:: clearer
|
||||
|
||||
..
|
||||
|
||||
Missing split adjustment
|
||||
------------------------
|
||||
|
||||
If stock split in data but preceding price data is not adjusted, then manually apply stock split.
|
||||
Requires date range include 1 day after stock split for calibration - sometimes Yahoo fails to adjust prices on stock split day.
|
||||
|
||||
.. figure:: /_static/images/repair-prices-missing-split-adjust.png
|
||||
:alt: MOB.ST: repair missing split adjustment
|
||||
:width: 80%
|
||||
:align: left
|
||||
|
||||
MOB.ST
|
||||
|
||||
.. container:: clearer
|
||||
|
||||
..
|
||||
|
||||
Missing data
|
||||
------------
|
||||
|
||||
If price data is clearly missing or corrupt, then reconstructed using smaller interval e.g. ``1h`` to fix ``1d`` data.
|
||||
|
||||
.. figure:: /_static/images/repair-prices-missing-row.png
|
||||
:alt: 1COV.DE: repair missing row
|
||||
:width: 80%
|
||||
:align: left
|
||||
|
||||
1COV.DE missing row
|
||||
|
||||
.. container:: clearer
|
||||
|
||||
..
|
||||
|
||||
.. figure:: /_static/images/repair-prices-missing-volume-intraday.png
|
||||
:alt: 1COV.DE: repair missing Volume, but intraday price changed
|
||||
:width: 80%
|
||||
:align: left
|
||||
|
||||
1COV.DE missing Volume, but intraday price changed
|
||||
|
||||
.. container:: clearer
|
||||
|
||||
..
|
||||
|
||||
.. figure:: /_static/images/repair-prices-missing-volume-daily.png
|
||||
:alt: 0316.HK: repair missing Volume, but daily price changed
|
||||
:width: 80%
|
||||
:align: left
|
||||
|
||||
0316.HK missing Volume, but daily price changed
|
||||
|
||||
.. container:: clearer
|
||||
|
||||
..
|
||||
|
||||
100x errors
|
||||
-----------
|
||||
|
||||
Sometimes Yahoo mixes up currencies e.g. $/cents or £/pence. So some prices are 100x wrong.
|
||||
Sometimes they are spread randomly through data - these detected with ``scipy`` module.
|
||||
Other times they are in a block, because Yahoo decided one day to permanently switch currency.
|
||||
|
||||
.. figure:: /_static/images/repair-prices-100x.png
|
||||
:alt: AET.L: repair 100x
|
||||
:width: 80%
|
||||
:align: left
|
||||
|
||||
AET.L
|
||||
|
||||
Price reconstruction - algorithm notes
|
||||
--------------------------------------
|
||||
|
||||
Spam minimised by grouping fetches. Tries to be aware of data limits e.g. ``1h`` cannot be fetched beyond 2 years.
|
||||
|
||||
If Yahoo eventually does fix the bad data that required reconstruction, you will see it's slightly different to reconstructed prices and volume often significantly different. Best I can do, and beats missing data.
|
||||
|
||||
Dividend repair (new)
|
||||
=====================
|
||||
|
||||
Fix errors in dividends:
|
||||
|
||||
1. adjustment missing or 100x too small/big for the dividend
|
||||
2. duplicate dividend (within 7 days)
|
||||
3. dividend 100x too big/small for the ex-dividend price drop
|
||||
4. ex-div date wrong (price drop is few days/weeks after)
|
||||
|
||||
Most errors I've seen are on London stock exchange (£/pence mixup), but no exchange is safe.
|
||||
|
||||
IMPORTANT - false positives
|
||||
---------------------------
|
||||
|
||||
Because fixing (3) relies on price action, there is a chance of a "false positive" (FP) - thinking an error exists when data is good.
|
||||
FP rate increases with longer intervals, so only 1d intervals are repaired. If you request repair on multiday intervals (weekly etc), then: 1d is fetched from Yahoo, repaired, then resampled - **this has nice side-effect of solving Yahoo's flawed way of div-adjusting multiday intervals.**
|
||||
|
||||
FP rate on 1d is tiny. They tend to happen with tiny dividends e.g. 0.5%, mistaking normal price volatility for an ex-div drop 100x bigger than the dividend, causing repair of the "too small" dividend (repair logic already tries to account for normal volatility by subtracting median). Either accept the risk, or fetch 6-12 months of prices with at least 2 dividends - then can analyse the dividends together to identify false positives.
|
||||
|
||||
Adjustment missing
|
||||
------------------
|
||||
|
||||
1398.HK
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
# ORIGINAL:
|
||||
Close Adj Close Dividends
|
||||
2024-07-08 00:00:00+08:00 4.33 4.33 0.335715
|
||||
2024-07-04 00:00:00+08:00 4.83 4.83 0.000000
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
# REPAIRED:
|
||||
Close Adj Close Dividends
|
||||
2024-07-08 00:00:00+08:00 4.33 4.330000 0.335715
|
||||
2024-07-04 00:00:00+08:00 4.83 4.494285 0.000000
|
||||
|
||||
Adjustment too small
|
||||
--------------------
|
||||
|
||||
3IN.L
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
# ORIGINAL:
|
||||
Close Adj Close Dividends
|
||||
2024-06-13 00:00:00+01:00 3.185 3.185000 0.05950
|
||||
2024-06-12 00:00:00+01:00 3.270 3.269405 0.00000
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
# REPAIRED:
|
||||
Close Adj Close Dividends
|
||||
2024-06-13 00:00:00+01:00 3.185 3.185000 0.05950
|
||||
2024-06-12 00:00:00+01:00 3.270 3.210500 0.00000
|
||||
|
||||
Duplicate (within 7 days)
|
||||
-------------------------
|
||||
|
||||
ALC.SW
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
# ORIGINAL:
|
||||
Close Adj Close Dividends
|
||||
2023-05-10 00:00:00+02:00 70.580002 70.352142 0.21
|
||||
2023-05-09 00:00:00+02:00 65.739998 65.318443 0.21
|
||||
2023-05-08 00:00:00+02:00 66.379997 65.745682 0.00
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
# REPAIRED:
|
||||
Close Adj Close Dividends
|
||||
2023-05-10 00:00:00+02:00 70.580002 70.352142 0.00
|
||||
2023-05-09 00:00:00+02:00 65.739998 65.527764 0.21
|
||||
2023-05-08 00:00:00+02:00 66.379997 65.956371 0.00
|
||||
|
||||
Dividend too big
|
||||
----------------
|
||||
|
||||
HLCL.L
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
# ORIGINAL:
|
||||
Close Adj Close Dividends
|
||||
2024-06-27 00:00:00+01:00 2.360 2.3600 1.78
|
||||
2024-06-26 00:00:00+01:00 2.375 2.3572 0.00
|
||||
|
||||
# REPAIRED:
|
||||
Close Adj Close Dividends
|
||||
2024-06-27 00:00:00+01:00 2.360 2.3600 0.0178
|
||||
2024-06-26 00:00:00+01:00 2.375 2.3572 0.0000
|
||||
|
||||
Dividend & adjust too big
|
||||
-------------------------
|
||||
|
||||
LTI.L
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
# ORIGINAL:
|
||||
Close Adj Close Adj Dividends
|
||||
2024-08-08 00:00:00+01:00 768.0 768.0 1.0000 5150.0
|
||||
2024-08-07 00:00:00+01:00 819.0 -4331.0 -5.2882 0.0
|
||||
Close Adj Close Adj Dividends
|
||||
2024-08-08 00:00:00+01:00 768.0 768.0 1.0000 51.5
|
||||
2024-08-07 00:00:00+01:00 819.0 767.5 0.9371 0.0
|
||||
|
||||
Dividend too small
|
||||
------------------
|
||||
|
||||
BVT.L
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
# ORIGINAL:
|
||||
Close Adj Close Adj Dividends
|
||||
2022-02-03 00:00:00+00:00 0.7534 0.675197 0.8962 0.00001
|
||||
2022-02-01 00:00:00+00:00 0.7844 0.702970 0.8962 0.00000
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
# REPAIRED:
|
||||
Close Adj Close Adj Dividends
|
||||
2022-02-03 00:00:00+00:00 0.7534 0.675197 0.8962 0.001
|
||||
2022-02-01 00:00:00+00:00 0.7844 0.702075 0.8950 0.000
|
||||
|
||||
Adjusted 2x on day before
|
||||
-------------------------
|
||||
|
||||
clue: Close < Low
|
||||
|
||||
2020.OL
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
# ORIGINAL:
|
||||
Low Close Adj Close Dividends
|
||||
2023-12-21 00:00:00+01:00 120.199997 121.099998 118.868782 0.18
|
||||
2023-12-20 00:00:00+01:00 122.000000 121.900002 119.477371 0.00
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
# REPAIRED:
|
||||
Low Close Adj Close Dividends
|
||||
2023-12-21 00:00:00+01:00 120.199997 121.099998 118.868782 0.18
|
||||
2023-12-20 00:00:00+01:00 122.000000 122.080002 119.654045 0.00
|
||||
|
||||
ex-div date wrong
|
||||
-----------------
|
||||
|
||||
TETY.ST
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
# ORIGINAL:
|
||||
Close Adj Close Dividends
|
||||
2022-06-22 00:00:00+02:00 66.699997 60.085415 0.0
|
||||
2022-06-21 00:00:00+02:00 71.599998 64.499489 0.0
|
||||
2022-06-20 00:00:00+02:00 71.800003 64.679657 5.0
|
||||
2022-06-17 00:00:00+02:00 71.000000 59.454838 0.0
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
# REPAIRED:
|
||||
Close Adj Close Dividends
|
||||
2022-06-22 00:00:00+02:00 66.699997 60.085415 5.0
|
||||
2022-06-21 00:00:00+02:00 71.599998 60.007881 0.0
|
||||
2022-06-20 00:00:00+02:00 71.800003 60.175503 0.0
|
||||
2022-06-17 00:00:00+02:00 71.000000 59.505021 0.0
|
||||
90
doc/source/development/code.rst
Normal file
90
doc/source/development/code.rst
Normal file
@@ -0,0 +1,90 @@
|
||||
****
|
||||
Code
|
||||
****
|
||||
|
||||
To support rapid development without breaking stable versions, this project uses a two-layer branch model:
|
||||
|
||||
.. image:: assets/branches.png
|
||||
:alt: Branching Model
|
||||
|
||||
`Inspiration <https://miro.medium.com/max/700/1*2YagIpX6LuauC3ASpwHekg.png>`_
|
||||
|
||||
- **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
|
||||
- Not updating the code (e.g. docs)
|
||||
|
||||
Creating your branch
|
||||
--------------------
|
||||
|
||||
1. Fork the repository on GitHub. If already forked, remember to ``Sync fork``
|
||||
|
||||
2. Clone your forked repository:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
git clone https://github.com/{user}/{repo}.git
|
||||
|
||||
3. Create a new branch for your feature or bug fix, from appropriate base branch:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
git checkout {base e.g. dev}
|
||||
git pull
|
||||
git checkout -b {your branch}
|
||||
|
||||
4. Make your changes, commit them, and push your branch to GitHub. To keep the commit history and `network graph <https://github.com/ranaroussi/yfinance/network>`_ compact, give your commits a very short summary then description:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
git commit -m "short sentence summary" -m "full commit message"
|
||||
# Long message can be multiple lines (tip: copy-paste)
|
||||
|
||||
6. `Open a pull request on Github <https://github.com/ranaroussi/yfinance/pulls>`_.
|
||||
|
||||
Running a branch
|
||||
----------------
|
||||
|
||||
Please see `this page </development/running>`_.
|
||||
|
||||
Git stuff
|
||||
---------
|
||||
|
||||
- You might be asked to move your branch from ``main`` to ``dev``. This is a ``git rebase``. Remember to update **all** branches involved.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# update all branches:
|
||||
git checkout main
|
||||
git pull
|
||||
git checkout dev
|
||||
git pull
|
||||
# rebase from main to dev:
|
||||
git checkout {your branch}
|
||||
git pull
|
||||
git rebase --onto dev main {your branch}
|
||||
git push --force-with-lease origin {your branch}
|
||||
|
||||
- ``git rebase`` can also be used to update your branch with new commits from base, but without adding a commit to your branch history like git merge does. This keeps history clean and avoids future merge problems.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
git checkout {base branch e.g. dev}
|
||||
git pull
|
||||
git checkout {your branch}
|
||||
git rebase {base}
|
||||
git push --force-with-lease origin {your branch}
|
||||
|
||||
- ``git squash`` tiny or negligible commits with meaningful ones, or to combine successive related commits. `git squash guide <https://docs.gitlab.com/ee/topics/git/git_rebase.html#interactive-rebase>`_
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
git rebase -i HEAD~2
|
||||
git push --force-with-lease origin {your branch}
|
||||
@@ -1,109 +0,0 @@
|
||||
********************************
|
||||
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 <https://github.com/ranaroussi/yfinance/network>`_ 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 <branch-name>
|
||||
|
||||
5. Open a pull request on the `yfinance` GitHub page.
|
||||
|
||||
For more information, see the `Developer Guide <https://github.com/ranaroussi/yfinance/discussions/1084>`_.
|
||||
|
||||
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 <https://miro.medium.com/max/700/1*2YagIpX6LuauC3ASpwHekg.png>`_
|
||||
|
||||
- **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 <your-branch>
|
||||
git rebase --onto dev main <branch-name>
|
||||
git push --force-with-lease origin <branch-name>
|
||||
|
||||
Running the GitHub Version of yfinance
|
||||
--------------------------------------
|
||||
|
||||
To download and run a GitHub version of `yfinance`, refer to `GitHub discussion <https://github.com/ranaroussi/yfinance/discussions/1080>`_
|
||||
@@ -1,15 +1,15 @@
|
||||
*************************************
|
||||
Contribution to the documentation
|
||||
*************************************
|
||||
*************
|
||||
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.
|
||||
* 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
|
||||
-------------------------------
|
||||
@@ -17,30 +17,38 @@ 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
|
||||
* Make sure ``Sphinx`` and any other dependencies are installed. If a ``requirements.txt`` file is available, you can install dependencies by running:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
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
|
||||
|
||||
|
||||
2. **Build with Sphinx**:
|
||||
|
||||
* After dependencies are installed, use the sphinx-build command to generate HTML documentation.
|
||||
* Go to `doc/` directory Run:
|
||||
* Go to ``doc/`` directory Run:
|
||||
|
||||
.. code-block:: console
|
||||
.. code-block:: bash
|
||||
|
||||
make clean && make html
|
||||
sphinx-build -b html doc/source doc/_build/html
|
||||
|
||||
3. **View Documentation Locally**:
|
||||
|
||||
* Open `doc/build/html/index.html` in the browser to view the generated documentation.
|
||||
.. code-block:: bash
|
||||
|
||||
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`.
|
||||
python -m http.server -d ./doc/_build/html
|
||||
|
||||
1. Reivew the changes locally and push to `dev`.
|
||||
Then open "localhost:8000" in browser
|
||||
|
||||
2. When `dev` gets merged to `main`, GitHub Actions workflow is automated to build documentation.
|
||||
|
||||
Publishing documentation
|
||||
------------------------
|
||||
|
||||
Merge into ``main`` branch triggers auto-generating documentation by action ``.github/workflows/deploy_doc.yml``.
|
||||
This publishes the generated HTML into branch ``documentation``.
|
||||
|
||||
1. Review the changes locally and push to ``dev``.
|
||||
|
||||
2. When ``dev`` gets merged to ``main``, GitHub Actions workflow is automated to build documentation.
|
||||
@@ -2,9 +2,12 @@
|
||||
Development
|
||||
===========
|
||||
|
||||
yfinance relies on the community to investigate bugs and contribute code. Here's how you can help:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
contributing
|
||||
code
|
||||
running
|
||||
documentation
|
||||
reporting_bug
|
||||
testing
|
||||
@@ -1,5 +0,0 @@
|
||||
********************************
|
||||
Reporting a Bug
|
||||
********************************
|
||||
|
||||
Open a new issue on our `GitHub <https://github.com/ranaroussi/yfinance/issues>`_.
|
||||
62
doc/source/development/running.rst
Normal file
62
doc/source/development/running.rst
Normal file
@@ -0,0 +1,62 @@
|
||||
Running a branch
|
||||
================
|
||||
|
||||
With PIP
|
||||
--------
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip install git+https://github.com/{user}/{repo}.git@{branch}
|
||||
|
||||
E.g.:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip install git+https://github.com/ranaroussi/yfinance.git@feature/name
|
||||
|
||||
With Git
|
||||
--------
|
||||
|
||||
1: Download from GitHub:
|
||||
|
||||
.. code-block:: bash
|
||||
git clone https://github.com/{user}/{repo}.git
|
||||
pip install -r ./yfinance/requirements.txt
|
||||
|
||||
Or if a specific branch:
|
||||
|
||||
.. code-block:: bash
|
||||
git clone -b {branch} https://github.com/{user}/{repo}.git
|
||||
pip install -r ./yfinance/requirements.txt
|
||||
|
||||
.. NOTE::
|
||||
Only do the next part if you are installing globally
|
||||
|
||||
If you are installing for 1 specific project, then you can skip this step
|
||||
and just `git clone` in the project directory
|
||||
|
||||
2. Add download location to Python search path
|
||||
|
||||
Two different ways, choose one:
|
||||
|
||||
1) Add path to ``PYTHONPATH`` environment variable
|
||||
|
||||
2) Add to top of Python file:
|
||||
.. code-block:: python
|
||||
import sys
|
||||
sys.path.insert(0, "path/to/downloaded/yfinance")
|
||||
|
||||
|
||||
3: Verify
|
||||
|
||||
.. code-block:: python
|
||||
import yfinance
|
||||
print(yfinance)
|
||||
|
||||
Output should be:
|
||||
|
||||
`<module 'yfinance' from 'path/to/downloaded/yfinance/yfinance/__init__.py'>`
|
||||
|
||||
If output looks like this then you did step 2 wrong
|
||||
|
||||
`<module 'yfinance' from '.../lib/python3.10/site-packages/yfinance/__init__.py'>`
|
||||
50
doc/source/development/testing.rst
Normal file
50
doc/source/development/testing.rst
Normal file
@@ -0,0 +1,50 @@
|
||||
Unit Tests
|
||||
----------
|
||||
|
||||
Tests are written using Python's `unittest` module. Here are some ways to run tests:
|
||||
|
||||
- **Run all price tests**:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
python -m unittest tests.test_prices
|
||||
|
||||
- **Run a subset of price tests**:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
python -m unittest tests.test_prices.TestPriceRepair
|
||||
|
||||
- **Run a specific test**:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
python -m unittest tests.test_prices_repair.TestPriceRepair.test_ticker_missing
|
||||
|
||||
- **General command**:
|
||||
|
||||
..code-block:: bash
|
||||
|
||||
python -m unittest tests.{file}.{class}.{method}
|
||||
|
||||
- **Run all tests**:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
python -m unittest discover -s tests
|
||||
|
||||
.. note::
|
||||
|
||||
The tests are currently failing already
|
||||
|
||||
Standard result:
|
||||
|
||||
**Failures:** 11
|
||||
|
||||
**Errors:** 93
|
||||
|
||||
**Skipped:** 1
|
||||
|
||||
.. seealso::
|
||||
|
||||
See the ` ``unittest`` module <https://docs.python.org/3/library/unittest.html>`_ for more information.
|
||||
@@ -23,7 +23,6 @@ The following are the publicly available classes, and functions exposed by the `
|
||||
- :class:`AsyncWebSocket <yfinance.AsyncWebSocket>`: Class for asynchronously streaming live market data.
|
||||
- :attr:`Sector <yfinance.Sector>`: Domain class for accessing sector information.
|
||||
- :attr:`Industry <yfinance.Industry>`: Domain class for accessing industry information.
|
||||
- :attr:`Market <yfinance.Market>`: Class for accessing market status & summary.
|
||||
- :attr:`EquityQuery <yfinance.EquityQuery>`: Class to build equity query filters.
|
||||
- :attr:`FundQuery <yfinance.FundQuery>`: Class to build fund query filters.
|
||||
- :attr:`screen <yfinance.screen>`: Run equity/fund queries.
|
||||
@@ -40,7 +39,6 @@ The following are the publicly available classes, and functions exposed by the `
|
||||
yfinance.market
|
||||
yfinance.financials
|
||||
yfinance.analysis
|
||||
yfinance.market
|
||||
yfinance.search
|
||||
yfinance.lookup
|
||||
yfinance.websocket
|
||||
|
||||
@@ -20,6 +20,9 @@ Ticker stock methods
|
||||
:meth:`yfinance.scrapers.history.PriceHistory.history`
|
||||
Documentation for history
|
||||
|
||||
:doc:`../advanced/price_repair`
|
||||
Documentation for price repair
|
||||
|
||||
.. autosummary::
|
||||
:toctree: api/
|
||||
:recursive:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% set name = "yfinance" %}
|
||||
{% set version = "0.2.58" %}
|
||||
{% set version = "0.2.66" %}
|
||||
|
||||
package:
|
||||
name: "{{ name|lower }}"
|
||||
|
||||
@@ -11,5 +11,5 @@ requests_cache>=1.0
|
||||
requests_ratelimiter>=0.3.1
|
||||
scipy>=1.6.3
|
||||
curl_cffi>=0.7
|
||||
protobuf>=5.29.0,<6
|
||||
websockets>=11.0
|
||||
protobuf>=3.19.0
|
||||
websockets>=13.0
|
||||
|
||||
2
setup.py
2
setup.py
@@ -64,7 +64,7 @@ setup(
|
||||
'platformdirs>=2.0.0', 'pytz>=2022.5',
|
||||
'frozendict>=2.3.4', 'peewee>=3.16.2',
|
||||
'beautifulsoup4>=4.11.1', 'curl_cffi>=0.7',
|
||||
'protobuf>=5.29.0,<6', 'websockets>=11.0'],
|
||||
'protobuf>=3.19.0', 'websockets>=13.0'],
|
||||
extras_require={
|
||||
'nospam': ['requests_cache>=1.0', 'requests_ratelimiter>=0.3.1'],
|
||||
'repair': ['scipy>=1.6.3'],
|
||||
|
||||
@@ -5,8 +5,8 @@ import datetime as _dt
|
||||
import sys
|
||||
import os
|
||||
import yfinance
|
||||
from requests_ratelimiter import LimiterSession
|
||||
from pyrate_limiter import Duration, RequestRate, Limiter
|
||||
# from requests_ratelimiter import LimiterSession
|
||||
# from pyrate_limiter import Duration, RequestRate, Limiter
|
||||
|
||||
_parent_dp = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
||||
_src_dp = _parent_dp
|
||||
@@ -25,12 +25,15 @@ if os.path.isdir(testing_cache_dirpath):
|
||||
import shutil
|
||||
shutil.rmtree(testing_cache_dirpath)
|
||||
|
||||
# Setup a session to only rate-limit
|
||||
history_rate = RequestRate(1, Duration.SECOND)
|
||||
limiter = Limiter(history_rate)
|
||||
session_gbl = LimiterSession(limiter=limiter)
|
||||
# Since switching to curl_cffi, the requests_ratelimiter|cache won't work.
|
||||
session_gbl = None
|
||||
|
||||
# Use this instead if you also want caching:
|
||||
# # Setup a session to only rate-limit
|
||||
# history_rate = RequestRate(1, Duration.SECOND)
|
||||
# limiter = Limiter(history_rate)
|
||||
# session_gbl = LimiterSession(limiter=limiter)
|
||||
|
||||
# # Use this instead if you also want caching:
|
||||
# from requests_cache import CacheMixin, SQLiteCache
|
||||
# from requests_ratelimiter import LimiterMixin
|
||||
# from requests import Session
|
||||
|
||||
@@ -224,7 +224,7 @@ class TestPriceHistory(unittest.TestCase):
|
||||
start_d = _dt.date(2022, 1, 1)
|
||||
end_d = _dt.date(2023, 1, 1)
|
||||
|
||||
tkr_div_dates = {'BHP.AX': [_dt.date(2022, 9, 1), _dt.date(2022, 2, 24)], # Yahoo claims 23-Feb but wrong because DST
|
||||
tkr_div_dates = {'BHP.AX': [_dt.date(2022, 9, 1), _dt.date(2022, 2, 24)],
|
||||
'IMP.JO': [_dt.date(2022, 9, 21), _dt.date(2022, 3, 16)],
|
||||
'BP.L': [_dt.date(2022, 11, 10), _dt.date(2022, 8, 11), _dt.date(2022, 5, 12),
|
||||
_dt.date(2022, 2, 17)],
|
||||
|
||||
@@ -18,9 +18,9 @@ from yfinance.exceptions import YFPricesMissingError, YFInvalidPeriodError, YFNo
|
||||
|
||||
|
||||
import unittest
|
||||
import requests_cache
|
||||
# import requests_cache
|
||||
from typing import Union, Any, get_args, _GenericAlias
|
||||
from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
|
||||
# from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
|
||||
|
||||
ticker_attributes = (
|
||||
("major_holders", pd.DataFrame),
|
||||
@@ -234,6 +234,23 @@ class TestTicker(unittest.TestCase):
|
||||
for attribute_name, attribute_type in ticker_attributes:
|
||||
assert_attribute_type(self, dat, attribute_name, attribute_type)
|
||||
|
||||
def test_ticker_with_symbol_mic(self):
|
||||
equities = [
|
||||
("OR", "XPAR"), # L'Oréal on Euronext Paris
|
||||
("AAPL", "XNYS"), # Apple on NYSE
|
||||
("GOOGL", "XNAS"), # Alphabet on NASDAQ
|
||||
("BMW", "XETR"), # BMW on XETRA
|
||||
]
|
||||
for eq in equities:
|
||||
# No exception = pass
|
||||
yf.Ticker(eq)
|
||||
yf.Ticker((eq[0], eq[1].lower()))
|
||||
|
||||
def test_ticker_with_symbol_mic_invalid(self):
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
yf.Ticker(('ABC', 'XXXX'))
|
||||
self.assertIn("Unknown MIC code: 'XXXX'", str(cm.exception))
|
||||
|
||||
|
||||
class TestTickerHistory(unittest.TestCase):
|
||||
session = None
|
||||
@@ -284,36 +301,37 @@ class TestTickerHistory(unittest.TestCase):
|
||||
else:
|
||||
self.assertIsInstance(data.columns, pd.MultiIndex)
|
||||
|
||||
def test_no_expensive_calls_introduced(self):
|
||||
"""
|
||||
Make sure calling history to get price data has not introduced more calls to yahoo than absolutely necessary.
|
||||
As doing other type of scraping calls than "query2.finance.yahoo.com/v8/finance/chart" to yahoo website
|
||||
will quickly trigger spam-block when doing bulk download of history data.
|
||||
"""
|
||||
symbol = "GOOGL"
|
||||
period = "1y"
|
||||
with requests_cache.CachedSession(backend="memory") as session:
|
||||
ticker = yf.Ticker(symbol, session=session)
|
||||
ticker.history(period=period)
|
||||
actual_urls_called = [r.url for r in session.cache.filter()]
|
||||
# Hopefully one day we find an equivalent "requests_cache" that works with "curl_cffi"
|
||||
# def test_no_expensive_calls_introduced(self):
|
||||
# """
|
||||
# Make sure calling history to get price data has not introduced more calls to yahoo than absolutely necessary.
|
||||
# As doing other type of scraping calls than "query2.finance.yahoo.com/v8/finance/chart" to yahoo website
|
||||
# will quickly trigger spam-block when doing bulk download of history data.
|
||||
# """
|
||||
# symbol = "GOOGL"
|
||||
# period = "1y"
|
||||
# with requests_cache.CachedSession(backend="memory") as session:
|
||||
# ticker = yf.Ticker(symbol, session=session)
|
||||
# ticker.history(period=period)
|
||||
# actual_urls_called = [r.url for r in session.cache.filter()]
|
||||
|
||||
# Remove 'crumb' argument
|
||||
for i in range(len(actual_urls_called)):
|
||||
u = actual_urls_called[i]
|
||||
parsed_url = urlparse(u)
|
||||
query_params = parse_qs(parsed_url.query)
|
||||
query_params.pop('crumb', None)
|
||||
query_params.pop('cookie', None)
|
||||
u = urlunparse(parsed_url._replace(query=urlencode(query_params, doseq=True)))
|
||||
actual_urls_called[i] = u
|
||||
actual_urls_called = tuple(actual_urls_called)
|
||||
# # Remove 'crumb' argument
|
||||
# for i in range(len(actual_urls_called)):
|
||||
# u = actual_urls_called[i]
|
||||
# parsed_url = urlparse(u)
|
||||
# query_params = parse_qs(parsed_url.query)
|
||||
# query_params.pop('crumb', None)
|
||||
# query_params.pop('cookie', None)
|
||||
# u = urlunparse(parsed_url._replace(query=urlencode(query_params, doseq=True)))
|
||||
# actual_urls_called[i] = u
|
||||
# actual_urls_called = tuple(actual_urls_called)
|
||||
|
||||
expected_urls = [
|
||||
f"https://query2.finance.yahoo.com/v8/finance/chart/{symbol}?interval=1d&range=1d", # ticker's tz
|
||||
f"https://query2.finance.yahoo.com/v8/finance/chart/{symbol}?events=div%2Csplits%2CcapitalGains&includePrePost=False&interval=1d&range={period}"
|
||||
]
|
||||
for url in actual_urls_called:
|
||||
self.assertTrue(url in expected_urls, f"Unexpected URL called: {url}")
|
||||
# expected_urls = [
|
||||
# f"https://query2.finance.yahoo.com/v8/finance/chart/{symbol}?interval=1d&range=1d", # ticker's tz
|
||||
# f"https://query2.finance.yahoo.com/v8/finance/chart/{symbol}?events=div%2Csplits%2CcapitalGains&includePrePost=False&interval=1d&range={period}"
|
||||
# ]
|
||||
# for url in actual_urls_called:
|
||||
# self.assertTrue(url in expected_urls, f"Unexpected URL called: {url}")
|
||||
|
||||
def test_dividends(self):
|
||||
data = self.ticker.dividends
|
||||
@@ -889,8 +907,6 @@ class TestTickerAnalysts(unittest.TestCase):
|
||||
data = self.ticker.upgrades_downgrades
|
||||
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.assertCountEqual(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
|
||||
@@ -984,7 +1000,7 @@ class TestTickerInfo(unittest.TestCase):
|
||||
self.symbols.append("QCSTIX") # good for testing, doesn't trade
|
||||
self.symbols += ["BTC-USD", "IWO", "VFINX", "^GSPC"]
|
||||
self.symbols += ["SOKE.IS", "ADS.DE"] # detected bugs
|
||||
self.symbols += ["EXTO", "NEPT" ] # Issues 2343 and 2363
|
||||
self.symbols += ["EXTO" ] # Issues 2343
|
||||
self.tickers = [yf.Ticker(s, session=self.session) for s in self.symbols]
|
||||
|
||||
def tearDown(self):
|
||||
@@ -1000,7 +1016,6 @@ class TestTickerInfo(unittest.TestCase):
|
||||
self.assertIsInstance(data, dict, "data has wrong type")
|
||||
expected_keys = ['industry', 'currentPrice', 'exchange', 'floatShares', 'companyOfficers', 'bid']
|
||||
for k in expected_keys:
|
||||
print(k)
|
||||
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")
|
||||
|
||||
@@ -1037,15 +1052,6 @@ class TestTickerInfo(unittest.TestCase):
|
||||
self.assertCountEqual(['quoteType', 'symbol', 'underlyingSymbol', 'uuid', 'maxAge', 'trailingPegRatio'], data.keys())
|
||||
self.assertIn("trailingPegRatio", data.keys(), "Did not find expected key 'trailingPegRatio' in info dict")
|
||||
|
||||
# Test issue 2363 (Empty QuoteResponse)
|
||||
data = self.tickers[11].info
|
||||
expected_keys = ['maxAge', 'priceHint', 'previousClose', 'open', 'dayLow', 'dayHigh', 'regularMarketPreviousClose',
|
||||
'regularMarketOpen', 'regularMarketDayLow', 'regularMarketDayHigh', 'volume', 'regularMarketVolume',
|
||||
'bid', 'ask', 'bidSize', 'askSize', 'fiftyTwoWeekLow', 'fiftyTwoWeekHigh', 'currency', 'tradeable',
|
||||
'exchange', 'quoteType', 'symbol', 'underlyingSymbol', 'shortName', 'timeZoneFullName', 'timeZoneShortName',
|
||||
'uuid', 'gmtOffSetMilliseconds', 'trailingPegRatio']
|
||||
self.assertCountEqual(expected_keys, data.keys())
|
||||
|
||||
# def test_fast_info_matches_info(self):
|
||||
# fast_info_keys = set()
|
||||
# for ticker in self.tickers:
|
||||
|
||||
@@ -15,7 +15,7 @@ import pandas as pd
|
||||
|
||||
import unittest
|
||||
|
||||
from yfinance.utils import is_valid_period_format, _dts_in_same_interval
|
||||
from yfinance.utils import is_valid_period_format, _dts_in_same_interval, _parse_user_dt
|
||||
|
||||
|
||||
class TestPandas(unittest.TestCase):
|
||||
@@ -169,6 +169,19 @@ class TestDateIntervalCheck(unittest.TestCase):
|
||||
dt3 = pd.Timestamp("2024-10-15 10:31:00")
|
||||
self.assertFalse(_dts_in_same_interval(dt1, dt3, "1min"))
|
||||
|
||||
def test_parse_user_dt(self):
|
||||
exchange_tz = "US/Eastern"
|
||||
dtstr = "2024-01-04"
|
||||
epoch = 1704344400 # output of `pd.Timestamp("2024-01-04", tz="US/Eastern").timestamp()`
|
||||
expected = pd.Timestamp(dtstr, tz=exchange_tz)
|
||||
|
||||
self.assertEqual(_parse_user_dt(epoch, exchange_tz), expected)
|
||||
self.assertEqual(_parse_user_dt(dtstr, exchange_tz), expected)
|
||||
self.assertEqual(_parse_user_dt(datetime(year=2024, month=1, day=4).date(), exchange_tz), expected)
|
||||
self.assertEqual(_parse_user_dt(datetime(year=2024, month=1, day=4), exchange_tz), expected)
|
||||
with self.assertRaises(ValueError):
|
||||
self.assertEqual(_parse_user_dt(float(epoch), exchange_tz), expected)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
|
||||
260
yfinance/base.py
260
yfinance/base.py
@@ -32,6 +32,7 @@ from curl_cffi import requests
|
||||
|
||||
|
||||
from . import utils, cache
|
||||
from .const import _MIC_TO_YAHOO_SUFFIX
|
||||
from .data import YfData
|
||||
from .exceptions import YFEarningsDateMissing, YFRateLimitError
|
||||
from .live import WebSocket
|
||||
@@ -44,11 +45,41 @@ from .scrapers.funds import FundsData
|
||||
|
||||
from .const import _BASE_URL_, _ROOT_URL_, _QUERY1_URL_, _SENTINEL_
|
||||
|
||||
from io import StringIO
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
|
||||
_tz_info_fetch_ctr = 0
|
||||
|
||||
class TickerBase:
|
||||
def __init__(self, ticker, session=None, proxy=_SENTINEL_):
|
||||
"""
|
||||
Initialize a Yahoo Finance Ticker object.
|
||||
|
||||
Args:
|
||||
ticker (str | tuple[str, str]):
|
||||
Yahoo Finance symbol (e.g. "AAPL")
|
||||
or a tuple of (symbol, MIC) e.g. ('OR','XPAR')
|
||||
(MIC = market identifier code)
|
||||
|
||||
session (requests.Session, optional):
|
||||
Custom requests session.
|
||||
"""
|
||||
if isinstance(ticker, tuple):
|
||||
if len(ticker) != 2:
|
||||
raise ValueError("Ticker tuple must be (symbol, mic_code)")
|
||||
base_symbol, mic_code = ticker
|
||||
# ticker = yahoo_ticker(base_symbol, mic_code)
|
||||
if mic_code.startswith('.'):
|
||||
mic_code = mic_code[1:]
|
||||
if mic_code.upper() not in _MIC_TO_YAHOO_SUFFIX:
|
||||
raise ValueError(f"Unknown MIC code: '{mic_code}'")
|
||||
sfx = _MIC_TO_YAHOO_SUFFIX[mic_code.upper()]
|
||||
if sfx != '':
|
||||
ticker = f'{base_symbol}.{sfx}'
|
||||
else:
|
||||
ticker = base_symbol
|
||||
|
||||
self.ticker = ticker.upper()
|
||||
self.session = session or requests.Session(impersonate="chrome")
|
||||
self._tz = None
|
||||
@@ -66,17 +97,22 @@ class TickerBase:
|
||||
if self.ticker == "":
|
||||
raise ValueError("Empty ticker name")
|
||||
|
||||
self._data: YfData = YfData(session=session)
|
||||
if proxy is not _SENTINEL_:
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
# accept isin as ticker
|
||||
if utils.is_isin(self.ticker):
|
||||
isin = self.ticker
|
||||
self.ticker = utils.get_ticker_by_isin(self.ticker, None, session)
|
||||
c = cache.get_isin_cache()
|
||||
self.ticker = c.lookup(isin)
|
||||
if not self.ticker:
|
||||
self.ticker = utils.get_ticker_by_isin(isin)
|
||||
if self.ticker == "":
|
||||
raise ValueError(f"Invalid ISIN number: {isin}")
|
||||
|
||||
self._data: YfData = YfData(session=session)
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
self._data._set_proxy(proxy)
|
||||
if self.ticker:
|
||||
c.store(isin, self.ticker)
|
||||
|
||||
# self._price_history = PriceHistory(self._data, self.ticker)
|
||||
self._price_history = None # lazy-load
|
||||
@@ -176,7 +212,7 @@ class TickerBase:
|
||||
Columns: period strongBuy buy hold sell strongSell
|
||||
"""
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
data = self._quote.recommendations
|
||||
@@ -186,7 +222,7 @@ class TickerBase:
|
||||
|
||||
def get_recommendations_summary(self, proxy=_SENTINEL_, as_dict=False):
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
return self.get_recommendations(as_dict=as_dict)
|
||||
@@ -198,7 +234,7 @@ class TickerBase:
|
||||
Columns: firm toGrade fromGrade action
|
||||
"""
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
data = self._quote.upgrades_downgrades
|
||||
@@ -208,21 +244,21 @@ class TickerBase:
|
||||
|
||||
def get_calendar(self, proxy=_SENTINEL_) -> dict:
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
return self._quote.calendar
|
||||
|
||||
def get_sec_filings(self, proxy=_SENTINEL_) -> dict:
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
return self._quote.sec_filings
|
||||
|
||||
def get_major_holders(self, proxy=_SENTINEL_, as_dict=False):
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
data = self._holders.major
|
||||
@@ -232,7 +268,7 @@ class TickerBase:
|
||||
|
||||
def get_institutional_holders(self, proxy=_SENTINEL_, as_dict=False):
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
data = self._holders.institutional
|
||||
@@ -243,7 +279,7 @@ class TickerBase:
|
||||
|
||||
def get_mutualfund_holders(self, proxy=_SENTINEL_, as_dict=False):
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
data = self._holders.mutualfund
|
||||
@@ -254,7 +290,7 @@ class TickerBase:
|
||||
|
||||
def get_insider_purchases(self, proxy=_SENTINEL_, as_dict=False):
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
data = self._holders.insider_purchases
|
||||
@@ -265,7 +301,7 @@ class TickerBase:
|
||||
|
||||
def get_insider_transactions(self, proxy=_SENTINEL_, as_dict=False):
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
data = self._holders.insider_transactions
|
||||
@@ -276,7 +312,7 @@ class TickerBase:
|
||||
|
||||
def get_insider_roster_holders(self, proxy=_SENTINEL_, as_dict=False):
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
data = self._holders.insider_roster
|
||||
@@ -287,7 +323,7 @@ class TickerBase:
|
||||
|
||||
def get_info(self, proxy=_SENTINEL_) -> dict:
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
data = self._quote.info
|
||||
@@ -295,21 +331,16 @@ class TickerBase:
|
||||
|
||||
def get_fast_info(self, proxy=_SENTINEL_):
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
if self._fast_info is None:
|
||||
self._fast_info = FastInfo(self)
|
||||
return self._fast_info
|
||||
|
||||
@property
|
||||
def basic_info(self):
|
||||
warnings.warn("'Ticker.basic_info' is deprecated and will be removed in future, Switch to 'Ticker.fast_info'", DeprecationWarning)
|
||||
return self.fast_info
|
||||
|
||||
def get_sustainability(self, proxy=_SENTINEL_, as_dict=False):
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
data = self._quote.sustainability
|
||||
@@ -319,7 +350,7 @@ class TickerBase:
|
||||
|
||||
def get_analyst_price_targets(self, proxy=_SENTINEL_) -> dict:
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
"""
|
||||
@@ -330,7 +361,7 @@ class TickerBase:
|
||||
|
||||
def get_earnings_estimate(self, proxy=_SENTINEL_, as_dict=False):
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
"""
|
||||
@@ -342,7 +373,7 @@ class TickerBase:
|
||||
|
||||
def get_revenue_estimate(self, proxy=_SENTINEL_, as_dict=False):
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
"""
|
||||
@@ -354,7 +385,7 @@ class TickerBase:
|
||||
|
||||
def get_earnings_history(self, proxy=_SENTINEL_, as_dict=False):
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
"""
|
||||
@@ -370,7 +401,7 @@ class TickerBase:
|
||||
Columns: current 7daysAgo 30daysAgo 60daysAgo 90daysAgo
|
||||
"""
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
data = self._analysis.eps_trend
|
||||
@@ -382,7 +413,7 @@ class TickerBase:
|
||||
Columns: upLast7days upLast30days downLast7days downLast30days
|
||||
"""
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
data = self._analysis.eps_revisions
|
||||
@@ -394,7 +425,7 @@ class TickerBase:
|
||||
Columns: stock industry sector index
|
||||
"""
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
data = self._analysis.growth_estimates
|
||||
@@ -411,7 +442,7 @@ class TickerBase:
|
||||
Default is "yearly"
|
||||
"""
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
if self._fundamentals.earnings is None:
|
||||
@@ -438,7 +469,7 @@ class TickerBase:
|
||||
Default is "yearly"
|
||||
"""
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
data = self._fundamentals.financials.get_income_time_series(freq=freq)
|
||||
@@ -452,14 +483,14 @@ class TickerBase:
|
||||
|
||||
def get_incomestmt(self, proxy=_SENTINEL_, as_dict=False, pretty=False, freq="yearly"):
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
return self.get_income_stmt(proxy, as_dict, pretty, freq)
|
||||
|
||||
def get_financials(self, proxy=_SENTINEL_, as_dict=False, pretty=False, freq="yearly"):
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
return self.get_income_stmt(proxy, as_dict, pretty, freq)
|
||||
@@ -478,7 +509,7 @@ class TickerBase:
|
||||
Default is "yearly"
|
||||
"""
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
|
||||
@@ -493,7 +524,7 @@ class TickerBase:
|
||||
|
||||
def get_balancesheet(self, proxy=_SENTINEL_, as_dict=False, pretty=False, freq="yearly"):
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
return self.get_balance_sheet(proxy, as_dict, pretty, freq)
|
||||
@@ -512,7 +543,7 @@ class TickerBase:
|
||||
Default is "yearly"
|
||||
"""
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
|
||||
@@ -527,37 +558,37 @@ class TickerBase:
|
||||
|
||||
def get_cashflow(self, proxy=_SENTINEL_, as_dict=False, pretty=False, freq="yearly"):
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
return self.get_cash_flow(proxy, as_dict, pretty, freq)
|
||||
|
||||
def get_dividends(self, proxy=_SENTINEL_, period="max") -> pd.Series:
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
return self._lazy_load_price_history().get_dividends(period=period)
|
||||
|
||||
def get_capital_gains(self, proxy=_SENTINEL_, period="max") -> pd.Series:
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
return self._lazy_load_price_history().get_capital_gains(period=period)
|
||||
|
||||
def get_splits(self, proxy=_SENTINEL_, period="max") -> pd.Series:
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
return self._lazy_load_price_history().get_splits(period=period)
|
||||
|
||||
def get_actions(self, proxy=_SENTINEL_, period="max") -> pd.Series:
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
return self._lazy_load_price_history().get_actions(period=period)
|
||||
|
||||
def get_shares(self, proxy=_SENTINEL_, as_dict=False) -> Union[pd.DataFrame, dict]:
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
data = self._fundamentals.shares
|
||||
@@ -570,18 +601,16 @@ class TickerBase:
|
||||
logger = utils.get_yf_logger()
|
||||
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
# Process dates
|
||||
tz = self._get_ticker_tz(timeout=10)
|
||||
dt_now = pd.Timestamp.utcnow().tz_convert(tz)
|
||||
if start is not None:
|
||||
start_ts = utils._parse_user_dt(start, tz)
|
||||
start = pd.Timestamp.fromtimestamp(start_ts).tz_localize("UTC").tz_convert(tz)
|
||||
start = utils._parse_user_dt(start, tz)
|
||||
if end is not None:
|
||||
end_ts = utils._parse_user_dt(end, tz)
|
||||
end = pd.Timestamp.fromtimestamp(end_ts).tz_localize("UTC").tz_convert(tz)
|
||||
end = utils._parse_user_dt(end, tz)
|
||||
if end is None:
|
||||
end = dt_now
|
||||
if start is None:
|
||||
@@ -624,7 +653,7 @@ class TickerBase:
|
||||
|
||||
def get_isin(self, proxy=_SENTINEL_) -> Optional[str]:
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
# *** experimental ***
|
||||
@@ -670,7 +699,7 @@ class TickerBase:
|
||||
logger = utils.get_yf_logger()
|
||||
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
tab_queryrefs = {
|
||||
@@ -707,10 +736,111 @@ class TickerBase:
|
||||
self._news = [article for article in news if not article.get('ad', [])]
|
||||
return self._news
|
||||
|
||||
def get_earnings_dates(self, limit = 12, offset = 0) -> Optional[pd.DataFrame]:
|
||||
return self._get_earnings_dates_using_scrape(limit, offset)
|
||||
|
||||
@utils.log_indent_decorator
|
||||
def get_earnings_dates(self, limit=12, proxy=_SENTINEL_) -> Optional[pd.DataFrame]:
|
||||
def _get_earnings_dates_using_scrape(self, limit = 12, offset = 0) -> Optional[pd.DataFrame]:
|
||||
"""
|
||||
Uses YfData.cache_get() to scrape earnings data from YahooFinance.
|
||||
(https://finance.yahoo.com/calendar/earnings?symbol=INTC)
|
||||
|
||||
Args:
|
||||
limit (int): Number of rows to extract (max=100)
|
||||
offset (int): if 0, search from future EPS estimates.
|
||||
if 1, search from the most recent EPS.
|
||||
if x, search from x'th recent EPS.
|
||||
|
||||
Returns:
|
||||
pd.DataFrame in the following format.
|
||||
|
||||
EPS Estimate Reported EPS Surprise(%)
|
||||
Date
|
||||
2025-10-30 2.97 - -
|
||||
2025-07-22 1.73 1.54 -10.88
|
||||
2025-05-06 2.63 2.7 2.57
|
||||
2025-02-06 2.09 2.42 16.06
|
||||
2024-10-31 1.92 1.55 -19.36
|
||||
... ... ... ...
|
||||
2014-07-31 0.61 0.65 7.38
|
||||
2014-05-01 0.55 0.68 22.92
|
||||
2014-02-13 0.55 0.58 6.36
|
||||
2013-10-31 0.51 0.54 6.86
|
||||
2013-08-01 0.46 0.5 7.86
|
||||
"""
|
||||
#####################################################
|
||||
# Define Constants
|
||||
#####################################################
|
||||
if limit > 0 and limit <= 25:
|
||||
size = 25
|
||||
elif limit > 25 and limit <= 50:
|
||||
size = 50
|
||||
elif limit > 50 and limit <= 100:
|
||||
size = 100
|
||||
else:
|
||||
raise ValueError("Please use limit <= 100")
|
||||
|
||||
# Define the URL
|
||||
url = "https://finance.yahoo.com/calendar/earnings?symbol={}&offset={}&size={}".format(
|
||||
self.ticker, offset, size
|
||||
)
|
||||
#####################################################
|
||||
# Get data
|
||||
#####################################################
|
||||
response = self._data.cache_get(url)
|
||||
|
||||
#####################################################
|
||||
# Response -> pd.DataFrame
|
||||
#####################################################
|
||||
# Parse the HTML content using BeautifulSoup
|
||||
soup = BeautifulSoup(response.text, "html.parser")
|
||||
# This page should have only one <table>
|
||||
table = soup.find("table")
|
||||
# If the table is found
|
||||
if table:
|
||||
# Get the HTML string of the table
|
||||
table_html = str(table)
|
||||
|
||||
# Wrap the HTML string in a StringIO object
|
||||
html_stringio = StringIO(table_html)
|
||||
|
||||
# Pass the StringIO object to pd.read_html()
|
||||
df = pd.read_html(html_stringio, na_values=['-'])[0]
|
||||
|
||||
# Drop redundant columns
|
||||
df = df.drop(["Symbol", "Company"], axis=1)
|
||||
|
||||
# Backwards compatibility
|
||||
df.rename(columns={'Surprise (%)': 'Surprise(%)'}, inplace=True)
|
||||
|
||||
df = df.dropna(subset="Earnings Date")
|
||||
|
||||
# Parse earnings date
|
||||
# - Pandas doesn't like EDT, EST
|
||||
df['Earnings Date'] = df['Earnings Date'].str.replace('EDT', 'America/New_York')
|
||||
df['Earnings Date'] = df['Earnings Date'].str.replace('EST', 'America/New_York')
|
||||
# - separate timezone string (last word)
|
||||
dt_parts = df['Earnings Date'].str.rsplit(' ', n=1, expand=True)
|
||||
dts = dt_parts[0]
|
||||
tzs = dt_parts[1]
|
||||
df['Earnings Date'] = pd.to_datetime(dts, format='%B %d, %Y at %I %p')
|
||||
df['Earnings Date'] = pd.Series([dt.tz_localize(tz) for dt, tz in zip(df['Earnings Date'], tzs)])
|
||||
df = df.set_index("Earnings Date")
|
||||
|
||||
else:
|
||||
err_msg = "No earnings dates found, symbol may be delisted"
|
||||
logger = utils.get_yf_logger()
|
||||
logger.error(f'{self.ticker}: {err_msg}')
|
||||
return None
|
||||
return df
|
||||
|
||||
@utils.log_indent_decorator
|
||||
def _get_earnings_dates_using_screener(self, limit=12, proxy=_SENTINEL_) -> Optional[pd.DataFrame]:
|
||||
"""
|
||||
Get earning dates (future and historic)
|
||||
|
||||
In Summer 2025, Yahoo stopped updating the data at this endpoint.
|
||||
So reverting to scraping HTML.
|
||||
|
||||
Args:
|
||||
limit (int): max amount of upcoming and recent earnings dates to return.
|
||||
@@ -722,7 +852,7 @@ class TickerBase:
|
||||
logger = utils.get_yf_logger()
|
||||
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
clamped_limit = min(limit, 100) # YF caps at 100, don't go higher
|
||||
@@ -735,17 +865,11 @@ class TickerBase:
|
||||
params = {"lang": "en-US", "region": "US"}
|
||||
body = {
|
||||
"size": clamped_limit,
|
||||
"query": {
|
||||
"operator": "and",
|
||||
"operands": [
|
||||
{"operator": "eq", "operands": ["ticker", self.ticker]},
|
||||
{"operator": "eq", "operands": ["eventtype", "2"]}
|
||||
]
|
||||
},
|
||||
"query": { "operator": "eq", "operands": ["ticker", self.ticker] },
|
||||
"sortField": "startdatetime",
|
||||
"sortType": "DESC",
|
||||
"entityIdType": "earnings",
|
||||
"includeFields": ["startdatetime", "timeZoneShortName", "epsestimate", "epsactual", "epssurprisepct"]
|
||||
"includeFields": ["startdatetime", "timeZoneShortName", "epsestimate", "epsactual", "epssurprisepct", "eventtype"]
|
||||
}
|
||||
response = self._data.post(url, params=params, body=body)
|
||||
json_data = response.json()
|
||||
@@ -761,6 +885,14 @@ class TickerBase:
|
||||
logger.error(f'{self.ticker}: {err_msg}')
|
||||
return None
|
||||
|
||||
# Convert eventtype
|
||||
# - 1 = earnings call (manually confirmed)
|
||||
# - 2 = earnings report
|
||||
# - 11 = stockholders meeting (manually confirmed)
|
||||
df['Event Type'] = df['Event Type'].replace('^1$', 'Call', regex=True)
|
||||
df['Event Type'] = df['Event Type'].replace('^2$', 'Earnings', regex=True)
|
||||
df['Event Type'] = df['Event Type'].replace('^11$', 'Meeting', regex=True)
|
||||
|
||||
# Calculate earnings date
|
||||
df['Earnings Date'] = pd.to_datetime(df['Event Start Date'])
|
||||
tz = self._get_ticker_tz(timeout=30)
|
||||
@@ -783,14 +915,14 @@ class TickerBase:
|
||||
|
||||
def get_history_metadata(self, proxy=_SENTINEL_) -> dict:
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
return self._lazy_load_price_history().get_history_metadata(proxy)
|
||||
|
||||
def get_funds_data(self, proxy=_SENTINEL_) -> Optional[FundsData]:
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
if not self._funds_data:
|
||||
|
||||
@@ -3,13 +3,15 @@ from threading import Lock
|
||||
import os as _os
|
||||
import platformdirs as _ad
|
||||
import atexit as _atexit
|
||||
import datetime as _datetime
|
||||
import datetime as _dt
|
||||
import pickle as _pkl
|
||||
|
||||
from .utils import get_yf_logger
|
||||
|
||||
_cache_init_lock = Lock()
|
||||
|
||||
|
||||
|
||||
# --------------
|
||||
# TimeZone cache
|
||||
# --------------
|
||||
@@ -105,7 +107,7 @@ _atexit.register(_TzDBManager.close_db)
|
||||
|
||||
|
||||
tz_db_proxy = _peewee.Proxy()
|
||||
class _KV(_peewee.Model):
|
||||
class _TZ_KV(_peewee.Model):
|
||||
key = _peewee.CharField(primary_key=True)
|
||||
value = _peewee.CharField(null=True)
|
||||
|
||||
@@ -146,11 +148,11 @@ class _TzCache:
|
||||
db.connect()
|
||||
tz_db_proxy.initialize(db)
|
||||
try:
|
||||
db.create_tables([_KV])
|
||||
db.create_tables([_TZ_KV])
|
||||
except _peewee.OperationalError as e:
|
||||
if 'WITHOUT' in str(e):
|
||||
_KV._meta.without_rowid = False
|
||||
db.create_tables([_KV])
|
||||
_TZ_KV._meta.without_rowid = False
|
||||
db.create_tables([_TZ_KV])
|
||||
else:
|
||||
raise
|
||||
self.initialised = 1 # success
|
||||
@@ -166,8 +168,8 @@ class _TzCache:
|
||||
return None
|
||||
|
||||
try:
|
||||
return _KV.get(_KV.key == key).value
|
||||
except _KV.DoesNotExist:
|
||||
return _TZ_KV.get(_TZ_KV.key == key).value
|
||||
except _TZ_KV.DoesNotExist:
|
||||
return None
|
||||
|
||||
def store(self, key, value):
|
||||
@@ -185,18 +187,18 @@ class _TzCache:
|
||||
return
|
||||
try:
|
||||
if value is None:
|
||||
q = _KV.delete().where(_KV.key == key)
|
||||
q = _TZ_KV.delete().where(_TZ_KV.key == key)
|
||||
q.execute()
|
||||
return
|
||||
with db.atomic():
|
||||
_KV.insert(key=key, value=value).execute()
|
||||
_TZ_KV.insert(key=key, value=value).execute()
|
||||
except _peewee.IntegrityError:
|
||||
# Integrity error means the key already exists. Try updating the key.
|
||||
old_value = self.lookup(key)
|
||||
if old_value != value:
|
||||
get_yf_logger().debug(f"Value for key {key} changed from {old_value} to {value}.")
|
||||
with db.atomic():
|
||||
q = _KV.update(value=value).where(_KV.key == key)
|
||||
q = _TZ_KV.update(value=value).where(_TZ_KV.key == key)
|
||||
q.execute()
|
||||
|
||||
|
||||
@@ -301,16 +303,16 @@ class ISODateTimeField(_peewee.DateTimeField):
|
||||
# because user discovered peewee allowed an invalid datetime
|
||||
# to get written.
|
||||
def db_value(self, value):
|
||||
if value and isinstance(value, _datetime.datetime):
|
||||
if value and isinstance(value, _dt.datetime):
|
||||
return value.isoformat()
|
||||
return super().db_value(value)
|
||||
def python_value(self, value):
|
||||
if value and isinstance(value, str) and 'T' in value:
|
||||
return _datetime.datetime.fromisoformat(value)
|
||||
return _dt.datetime.fromisoformat(value)
|
||||
return super().python_value(value)
|
||||
class _CookieSchema(_peewee.Model):
|
||||
strategy = _peewee.CharField(primary_key=True)
|
||||
fetch_date = ISODateTimeField(default=_datetime.datetime.now)
|
||||
fetch_date = ISODateTimeField(default=_dt.datetime.now)
|
||||
|
||||
# Which cookie type depends on strategy
|
||||
cookie_bytes = _peewee.BlobField()
|
||||
@@ -374,7 +376,7 @@ class _CookieCache:
|
||||
try:
|
||||
data = _CookieSchema.get(_CookieSchema.strategy == strategy)
|
||||
cookie = _pkl.loads(data.cookie_bytes)
|
||||
return {'cookie':cookie, 'age':_datetime.datetime.now()-data.fetch_date}
|
||||
return {'cookie':cookie, 'age':_dt.datetime.now()-data.fetch_date}
|
||||
except _CookieSchema.DoesNotExist:
|
||||
return None
|
||||
|
||||
@@ -415,6 +417,211 @@ def get_cookie_cache():
|
||||
|
||||
|
||||
|
||||
# --------------
|
||||
# ISIN cache
|
||||
# --------------
|
||||
|
||||
class _ISINCacheException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class _ISINCacheDummy:
|
||||
"""Dummy cache to use if isin cache is disabled"""
|
||||
|
||||
def lookup(self, isin):
|
||||
return None
|
||||
|
||||
def store(self, isin, tkr):
|
||||
pass
|
||||
|
||||
@property
|
||||
def tz_db(self):
|
||||
return None
|
||||
|
||||
|
||||
class _ISINCacheManager:
|
||||
_isin_cache = None
|
||||
|
||||
@classmethod
|
||||
def get_isin_cache(cls):
|
||||
if cls._isin_cache is None:
|
||||
with _cache_init_lock:
|
||||
cls._initialise()
|
||||
return cls._isin_cache
|
||||
|
||||
@classmethod
|
||||
def _initialise(cls, cache_dir=None):
|
||||
cls._isin_cache = _ISINCache()
|
||||
|
||||
|
||||
class _ISINDBManager:
|
||||
_db = None
|
||||
_cache_dir = _os.path.join(_ad.user_cache_dir(), "py-yfinance")
|
||||
|
||||
@classmethod
|
||||
def get_database(cls):
|
||||
if cls._db is None:
|
||||
cls._initialise()
|
||||
return cls._db
|
||||
|
||||
@classmethod
|
||||
def close_db(cls):
|
||||
if cls._db is not None:
|
||||
try:
|
||||
cls._db.close()
|
||||
except Exception:
|
||||
# Must discard exceptions because Python trying to quit.
|
||||
pass
|
||||
|
||||
|
||||
@classmethod
|
||||
def _initialise(cls, cache_dir=None):
|
||||
if cache_dir is not None:
|
||||
cls._cache_dir = cache_dir
|
||||
|
||||
if not _os.path.isdir(cls._cache_dir):
|
||||
try:
|
||||
_os.makedirs(cls._cache_dir)
|
||||
except OSError as err:
|
||||
raise _ISINCacheException(f"Error creating ISINCache folder: '{cls._cache_dir}' reason: {err}")
|
||||
elif not (_os.access(cls._cache_dir, _os.R_OK) and _os.access(cls._cache_dir, _os.W_OK)):
|
||||
raise _ISINCacheException(f"Cannot read and write in ISINCache folder: '{cls._cache_dir}'")
|
||||
|
||||
cls._db = _peewee.SqliteDatabase(
|
||||
_os.path.join(cls._cache_dir, 'isin-tkr.db'),
|
||||
pragmas={'journal_mode': 'wal', 'cache_size': -64}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def set_location(cls, new_cache_dir):
|
||||
if cls._db is not None:
|
||||
cls._db.close()
|
||||
cls._db = None
|
||||
cls._cache_dir = new_cache_dir
|
||||
|
||||
@classmethod
|
||||
def get_location(cls):
|
||||
return cls._cache_dir
|
||||
|
||||
# close DB when Python exists
|
||||
_atexit.register(_ISINDBManager.close_db)
|
||||
|
||||
|
||||
isin_db_proxy = _peewee.Proxy()
|
||||
class _ISIN_KV(_peewee.Model):
|
||||
key = _peewee.CharField(primary_key=True)
|
||||
value = _peewee.CharField(null=True)
|
||||
created_at = _peewee.DateTimeField(default=_dt.datetime.now)
|
||||
|
||||
class Meta:
|
||||
database = isin_db_proxy
|
||||
without_rowid = True
|
||||
|
||||
|
||||
class _ISINCache:
|
||||
def __init__(self):
|
||||
self.initialised = -1
|
||||
self.db = None
|
||||
self.dummy = False
|
||||
|
||||
def get_db(self):
|
||||
if self.db is not None:
|
||||
return self.db
|
||||
|
||||
try:
|
||||
self.db = _ISINDBManager.get_database()
|
||||
except _ISINCacheException as err:
|
||||
get_yf_logger().info(f"Failed to create ISINCache, reason: {err}. "
|
||||
"ISINCache will not be used. "
|
||||
"Tip: You can direct cache to use a different location with 'set_isin_cache_location(mylocation)'")
|
||||
self.dummy = True
|
||||
return None
|
||||
return self.db
|
||||
|
||||
def initialise(self):
|
||||
if self.initialised != -1:
|
||||
return
|
||||
|
||||
db = self.get_db()
|
||||
if db is None:
|
||||
self.initialised = 0 # failure
|
||||
return
|
||||
|
||||
db.connect()
|
||||
isin_db_proxy.initialize(db)
|
||||
try:
|
||||
db.create_tables([_ISIN_KV])
|
||||
except _peewee.OperationalError as e:
|
||||
if 'WITHOUT' in str(e):
|
||||
_ISIN_KV._meta.without_rowid = False
|
||||
db.create_tables([_ISIN_KV])
|
||||
else:
|
||||
raise
|
||||
self.initialised = 1 # success
|
||||
|
||||
def lookup(self, key):
|
||||
if self.dummy:
|
||||
return None
|
||||
|
||||
if self.initialised == -1:
|
||||
self.initialise()
|
||||
|
||||
if self.initialised == 0: # failure
|
||||
return None
|
||||
|
||||
try:
|
||||
return _ISIN_KV.get(_ISIN_KV.key == key).value
|
||||
except _ISIN_KV.DoesNotExist:
|
||||
return None
|
||||
|
||||
def store(self, key, value):
|
||||
if self.dummy:
|
||||
return
|
||||
|
||||
if self.initialised == -1:
|
||||
self.initialise()
|
||||
|
||||
if self.initialised == 0: # failure
|
||||
return
|
||||
|
||||
db = self.get_db()
|
||||
if db is None:
|
||||
return
|
||||
try:
|
||||
if value is None:
|
||||
q = _ISIN_KV.delete().where(_ISIN_KV.key == key)
|
||||
q.execute()
|
||||
return
|
||||
|
||||
# Remove existing rows with same value that are older than 1 week
|
||||
one_week_ago = _dt.datetime.now() - _dt.timedelta(weeks=1)
|
||||
old_rows_query = _ISIN_KV.delete().where(
|
||||
(_ISIN_KV.value == value) &
|
||||
(_ISIN_KV.created_at < one_week_ago)
|
||||
)
|
||||
old_rows_query.execute()
|
||||
|
||||
with db.atomic():
|
||||
_ISIN_KV.insert(key=key, value=value).execute()
|
||||
|
||||
except _peewee.IntegrityError:
|
||||
# Integrity error means the key already exists. Try updating the key.
|
||||
old_value = self.lookup(key)
|
||||
if old_value != value:
|
||||
get_yf_logger().debug(f"Value for key {key} changed from {old_value} to {value}.")
|
||||
with db.atomic():
|
||||
q = _ISIN_KV.update(value=value, created_at=_dt.datetime.now()).where(_ISIN_KV.key == key)
|
||||
q.execute()
|
||||
|
||||
|
||||
def get_isin_cache():
|
||||
return _ISINCacheManager.get_isin_cache()
|
||||
|
||||
|
||||
# --------------
|
||||
# Utils
|
||||
# --------------
|
||||
|
||||
def set_cache_location(cache_dir: str):
|
||||
"""
|
||||
Sets the path to create the "py-yfinance" cache folder in.
|
||||
@@ -425,6 +632,7 @@ def set_cache_location(cache_dir: str):
|
||||
"""
|
||||
_TzDBManager.set_location(cache_dir)
|
||||
_CookieDBManager.set_location(cache_dir)
|
||||
_ISINDBManager.set_location(cache_dir)
|
||||
|
||||
def set_tz_cache_location(cache_dir: str):
|
||||
set_cache_location(cache_dir)
|
||||
|
||||
@@ -161,151 +161,221 @@ quote_summary_valid_modules = (
|
||||
|
||||
# map last updated as of 2024.09.18
|
||||
SECTOR_INDUSTY_MAPPING = {
|
||||
'basic-materials': {'specialty-chemicals',
|
||||
'gold',
|
||||
'building-materials',
|
||||
'copper',
|
||||
'steel',
|
||||
'agricultural-inputs',
|
||||
'chemicals',
|
||||
'other-industrial-metals-mining',
|
||||
'lumber-wood-production',
|
||||
'aluminum',
|
||||
'other-precious-metals-mining',
|
||||
'coking-coal',
|
||||
'paper-paper-products',
|
||||
'silver'},
|
||||
'communication-services': {'internet-content-information',
|
||||
'telecom-services',
|
||||
'entertainment',
|
||||
'electronic-gaming-multimedia',
|
||||
'advertising-agencies',
|
||||
'broadcasting',
|
||||
'publishing'},
|
||||
'consumer-cyclical': {'internet-retail',
|
||||
'auto-manufacturers',
|
||||
'restaurants',
|
||||
'home-improvement-retail',
|
||||
'travel-services',
|
||||
'specialty-retail',
|
||||
'apparel-retail',
|
||||
'residential-construction',
|
||||
'footwear-accessories',
|
||||
'packaging-containers',
|
||||
'lodging',
|
||||
'auto-parts',
|
||||
'auto-truck-dealerships',
|
||||
'gambling',
|
||||
'resorts-casinos',
|
||||
'leisure',
|
||||
'apparel-manufacturing',
|
||||
'personal-services',
|
||||
'furnishings-fixtures-appliances',
|
||||
'recreational-vehicles',
|
||||
'luxury-goods',
|
||||
'department-stores',
|
||||
'textile-manufacturing'},
|
||||
'consumer-defensive': {'discount-stores',
|
||||
'beverages-non-alcoholic',
|
||||
'household-personal-products',
|
||||
'packaged-foods',
|
||||
'tobacco',
|
||||
'confectioners',
|
||||
'farm-products',
|
||||
'food-distribution',
|
||||
'grocery-stores',
|
||||
'beverages-brewers',
|
||||
'education-training-services',
|
||||
'beverages-wineries-distilleries'},
|
||||
'energy': {'oil-gas-integrated',
|
||||
'oil-gas-midstream',
|
||||
'oil-gas-e-p',
|
||||
'oil-gas-equipment-services',
|
||||
'oil-gas-refining-marketing',
|
||||
'uranium',
|
||||
'oil-gas-drilling',
|
||||
'thermal-coal'},
|
||||
'financial-services': {'banks-diversified',
|
||||
'credit-services',
|
||||
'asset-management',
|
||||
'insurance-diversified',
|
||||
'banks-regional',
|
||||
'capital-markets',
|
||||
'financial-data-stock-exchanges',
|
||||
'insurance-property-casualty',
|
||||
'insurance-brokers',
|
||||
'insurance-life',
|
||||
'insurance-specialty',
|
||||
'mortgage-finance',
|
||||
'insurance-reinsurance',
|
||||
'shell-companies',
|
||||
'financial-conglomerates'},
|
||||
'healthcare': {'drug-manufacturers-general',
|
||||
'healthcare-plans',
|
||||
'biotechnology',
|
||||
'medical-devices',
|
||||
'diagnostics-research',
|
||||
'medical-instruments-supplies',
|
||||
'medical-care-facilities',
|
||||
'drug-manufacturers-specialty-generic',
|
||||
'health-information-services',
|
||||
'medical-distribution',
|
||||
'pharmaceutical-retailers'},
|
||||
'industrials': {'aerospace-defense',
|
||||
'specialty-industrial-machinery',
|
||||
'railroads',
|
||||
'building-products-equipment',
|
||||
'farm-heavy-construction-machinery',
|
||||
'specialty-business-services',
|
||||
'integrated-freight-logistics',
|
||||
'waste-management',
|
||||
'conglomerates',
|
||||
'industrial-distribution',
|
||||
'engineering-construction',
|
||||
'rental-leasing-services',
|
||||
'consulting-services',
|
||||
'trucking',
|
||||
'electrical-equipment-parts',
|
||||
'airlines',
|
||||
'tools-accessories',
|
||||
'pollution-treatment-controls',
|
||||
'security-protection-services',
|
||||
'marine-shipping',
|
||||
'metal-fabrication',
|
||||
'infrastructure-operations',
|
||||
'staffing-employment-services',
|
||||
'airports-air-services',
|
||||
'business-equipment-supplies'},
|
||||
'real-estate': {'reit-specialty',
|
||||
'reit-industrial',
|
||||
'reit-retail',
|
||||
'reit-residential',
|
||||
'reit-healthcare-facilities',
|
||||
'real-estate-services',
|
||||
'reit-office',
|
||||
'reit-diversified',
|
||||
'reit-mortgage',
|
||||
'reit-hotel-motel',
|
||||
'real-estate-development',
|
||||
'real-estate-diversified'},
|
||||
'technology': {'software-infrastructure',
|
||||
'semiconductors',
|
||||
'consumer-electronics',
|
||||
'software-application',
|
||||
'information-technology-services',
|
||||
'semiconductor-equipment-materials',
|
||||
'communication-equipment',
|
||||
'computer-hardware',
|
||||
'electronic-components',
|
||||
'scientific-technical-instruments',
|
||||
'solar',
|
||||
'electronics-computer-distribution'},
|
||||
'utilities': {'utilities-regulated-electric',
|
||||
'utilities-renewable',
|
||||
'utilities-diversified',
|
||||
'utilities-regulated-gas',
|
||||
'utilities-independent-power-producers',
|
||||
'utilities-regulated-water'}
|
||||
'Basic Materials': {'Specialty Chemicals',
|
||||
'Gold',
|
||||
'Building Materials',
|
||||
'Copper',
|
||||
'Steel',
|
||||
'Agricultural Inputs',
|
||||
'Chemicals',
|
||||
'Other Industrial Metals & Mining',
|
||||
'Lumber & Wood Production',
|
||||
'Aluminum',
|
||||
'Other Precious Metals & Mining',
|
||||
'Coking Coal',
|
||||
'Paper & Paper Products',
|
||||
'Silver'},
|
||||
'Communication Services': {'Advertising Agencies',
|
||||
'Broadcasting',
|
||||
'Electronic Gaming & Multimedia',
|
||||
'Entertainment',
|
||||
'Internet Content & Information',
|
||||
'Publishing',
|
||||
'Telecom Services'},
|
||||
'Consumer Cyclical': {'Apparel Manufacturing',
|
||||
'Apparel Retail',
|
||||
'Auto & Truck Dealerships',
|
||||
'Auto Manufacturers',
|
||||
'Auto Parts',
|
||||
'Department Stores',
|
||||
'Footwear & Accessories',
|
||||
'Furnishings, Fixtures & Appliances',
|
||||
'Gambling',
|
||||
'Home Improvement Retail',
|
||||
'Internet Retail',
|
||||
'Leisure',
|
||||
'Lodging',
|
||||
'Luxury Goods',
|
||||
'Packaging & Containers',
|
||||
'Personal Services',
|
||||
'Recreational Vehicles',
|
||||
'Residential Construction',
|
||||
'Resorts & Casinos',
|
||||
'Restaurants',
|
||||
'Specialty Retail',
|
||||
'Textile Manufacturing',
|
||||
'Travel Services'},
|
||||
'Consumer Defensive': {'Beverages - Brewers',
|
||||
'Beverages - Non-Alcoholic',
|
||||
'Beverages - Wineries & Distilleries',
|
||||
'Confectioners',
|
||||
'Discount Stores',
|
||||
'Education & Training Services',
|
||||
'Farm Products',
|
||||
'Food Distribution',
|
||||
'Grocery Stores',
|
||||
'Household & Personal Products',
|
||||
'Packaged Foods',
|
||||
'Tobacco'},
|
||||
'Energy': {'Oil Gas Drilling',
|
||||
'Oil Gas E P',
|
||||
'Oil Gas Equipment Services',
|
||||
'Oil Gas Integrated',
|
||||
'Oil Gas Midstream',
|
||||
'Oil Gas Refining Marketing',
|
||||
'Thermal Coal',
|
||||
'Uranium'},
|
||||
'Financial Services': {'Asset Management',
|
||||
'Banks Diversified',
|
||||
'Banks Regional',
|
||||
'Capital Markets',
|
||||
'Credit Services',
|
||||
'Financial Conglomerates',
|
||||
'Financial Data Stock Exchanges',
|
||||
'Insurance Brokers',
|
||||
'Insurance Diversified',
|
||||
'Insurance Life',
|
||||
'Insurance Property Casualty',
|
||||
'Insurance Reinsurance',
|
||||
'Insurance Specialty',
|
||||
'Mortgage Finance',
|
||||
'Shell Companies'},
|
||||
'Healthcare': {'Biotechnology',
|
||||
'Diagnostics Research',
|
||||
'Drug Manufacturers General',
|
||||
'Drug Manufacturers Specialty Generic',
|
||||
'Health Information Services',
|
||||
'Healthcare Plans',
|
||||
'Medical Care Facilities',
|
||||
'Medical Devices',
|
||||
'Medical Distribution',
|
||||
'Medical Instruments Supplies',
|
||||
'Pharmaceutical Retailers'},
|
||||
'Industrials': {'Aerospace Defense',
|
||||
'Airlines',
|
||||
'Airports Air Services',
|
||||
'Building Products Equipment',
|
||||
'Business Equipment Supplies',
|
||||
'Conglomerates',
|
||||
'Consulting Services',
|
||||
'Electrical Equipment Parts',
|
||||
'Engineering Construction',
|
||||
'Farm Heavy Construction Machinery',
|
||||
'Industrial Distribution',
|
||||
'Infrastructure Operations',
|
||||
'Integrated Freight Logistics',
|
||||
'Marine Shipping',
|
||||
'Metal Fabrication',
|
||||
'Pollution Treatment Controls',
|
||||
'Railroads',
|
||||
'Rental Leasing Services',
|
||||
'Security Protection Services',
|
||||
'Specialty Business Services',
|
||||
'Specialty Industrial Machinery',
|
||||
'Staffing Employment Services',
|
||||
'Tools Accessories',
|
||||
'Trucking',
|
||||
'Waste Management'},
|
||||
'Real Estate': {'Real Estate Development',
|
||||
'Real Estate Diversified',
|
||||
'Real Estate Services',
|
||||
'Reit Diversified',
|
||||
'Reit Healthcare Facilities',
|
||||
'Reit Hotel Motel',
|
||||
'Reit Industrial',
|
||||
'Reit Mortgage',
|
||||
'Reit Office',
|
||||
'Reit Residential',
|
||||
'Reit Retail',
|
||||
'Reit Specialty'},
|
||||
'Technology': {'Communication Equipment',
|
||||
'Computer Hardware',
|
||||
'Consumer Electronics',
|
||||
'Electronic Components',
|
||||
'Electronics Computer Distribution',
|
||||
'Information Technology Services',
|
||||
'Scientific Technical Instruments',
|
||||
'Semiconductor Equipment Materials',
|
||||
'Semiconductors',
|
||||
'Software Application',
|
||||
'Software Infrastructure',
|
||||
'Solar'},
|
||||
'Utilities': {'Utilities Diversified',
|
||||
'Utilities Independent Power Producers',
|
||||
'Utilities Regulated Electric',
|
||||
'Utilities Regulated Gas',
|
||||
'Utilities Regulated Water',
|
||||
'Utilities Renewable'},
|
||||
}
|
||||
SECTOR_INDUSTY_MAPPING_LC = {}
|
||||
for k in SECTOR_INDUSTY_MAPPING.keys():
|
||||
k2 = k.lower().replace('& ', '').replace('- ', '').replace(', ', ' ').replace(' ', '-')
|
||||
SECTOR_INDUSTY_MAPPING_LC[k2] = []
|
||||
for v in SECTOR_INDUSTY_MAPPING[k]:
|
||||
v2 = v.lower().replace('& ', '').replace('- ', '').replace(', ', ' ').replace(' ', '-')
|
||||
SECTOR_INDUSTY_MAPPING_LC[k2].append(v2)
|
||||
|
||||
# _MIC_TO_YAHOO_SUFFIX maps Market Identifier Codes (MIC) to Yahoo Finance market suffixes.
|
||||
# c.f. :
|
||||
# https://help.yahoo.com/kb/finance-for-web/SLN2310.html;_ylt=AwrJKiCZFo9g3Y8AsDWPAwx.;_ylu=Y29sbwMEcG9zAzEEdnRpZAMEc2VjA3Ny?locale=en_US
|
||||
# https://www.iso20022.org/market-identifier-codes
|
||||
|
||||
_MIC_TO_YAHOO_SUFFIX = {
|
||||
'XCBT': 'CBT', 'XCME': 'CME', 'IFUS': 'NYB', 'CECS': 'CMX', 'XNYM': 'NYM', 'XNYS': '', 'XNAS': '', # United States
|
||||
'XBUE': 'BA', # Argentina
|
||||
'XVIE': 'VI', # Austria
|
||||
'XASX': 'AX', 'XAUS': 'XA', # Australia
|
||||
'XBRU': 'BR', # Belgium
|
||||
'BVMF': 'SA', # Brazil
|
||||
'CNSX': 'CN', 'NEOE': 'NE', 'XTSE': 'TO', 'XTSX': 'V', # Canada
|
||||
'XSGO': 'SN', # Chile
|
||||
'XSHG': 'SS', 'XSHE': 'SZ', # China
|
||||
'XBOG': 'CL', # Colombia
|
||||
'XPRA': 'PR', # Czech Republic
|
||||
'XCSE': 'CO', # Denmark
|
||||
'XCAI': 'CA', # Egypt
|
||||
'XTAL': 'TL', # Estonia
|
||||
'CEUX': 'XD', 'XEUR': 'NX', # Europe (Cboe Europe, Euronext)
|
||||
'XHEL': 'HE', # Finland
|
||||
'XPAR': 'PA', # France
|
||||
'XBER': 'BE', 'XBMS': 'BM', 'XDUS': 'DU', 'XFRA': 'F', 'XHAM': 'HM', 'XHAN': 'HA', 'XMUN': 'MU', 'XSTU': 'SG', 'XETR': 'DE', # Germany
|
||||
'XATH': 'AT', # Greece
|
||||
'XHKG': 'HK', # Hong Kong
|
||||
'XBUD': 'BD', # Hungary
|
||||
'XICE': 'IC', # Iceland
|
||||
'XBOM': 'BO', 'XNSE': 'NS', # India
|
||||
'XIDX': 'JK', # Indonesia
|
||||
'XDUB': 'IR', # Ireland
|
||||
'XTAE': 'TA', # Israel
|
||||
'MTAA': 'MI', 'EUTL': 'TI', # Italy
|
||||
'XTKS': 'T', # Japan
|
||||
'XKFE': 'KW', # Kuwait
|
||||
'XRIS': 'RG', # Latvia
|
||||
'XVIL': 'VS', # Lithuania
|
||||
'XKLS': 'KL', # Malaysia
|
||||
'XMEX': 'MX', # Mexico
|
||||
'XAMS': 'AS', # Netherlands
|
||||
'XNZE': 'NZ', # New Zealand
|
||||
'XOSL': 'OL', # Norway
|
||||
'XPHS': 'PS', # Philippines
|
||||
'XWAR': 'WA', # Poland
|
||||
'XLIS': 'LS', # Portugal
|
||||
'XQAT': 'QA', # Qatar
|
||||
'XBSE': 'RO', # Romania
|
||||
'XSES': 'SI', # Singapore
|
||||
'XJSE': 'JO', # South Africa
|
||||
'XKRX': 'KS', 'KQKS': 'KQ', # South Korea
|
||||
'BMEX': 'MC', # Spain
|
||||
'XTAD': 'SAU', # Saudi Arabia
|
||||
'XSTO': 'ST', # Sweden
|
||||
'XSWX': 'SW', # Switzerland
|
||||
'ROCO': 'TWO', 'XTAI': 'TW', # Taiwan
|
||||
'XBKK': 'BK', # Thailand
|
||||
'XIST': 'IS', # Turkey
|
||||
'XDFM': 'AE', # UAE
|
||||
'AQXE': 'AQ', 'XCHI': 'XC', 'XLON': 'L', 'ILSE': 'IL', # United Kingdom
|
||||
'XCAR': 'CR', # Venezuela
|
||||
'XSTC': 'VN' # Vietnam
|
||||
}
|
||||
|
||||
def merge_two_level_dicts(dict1, dict2):
|
||||
@@ -380,6 +450,7 @@ EQUITY_SCREENER_EQ_MAP = {
|
||||
'se': {'STO'},
|
||||
'sg': {'SES'},
|
||||
'sr': {},
|
||||
'sw': {'EBS'},
|
||||
'th': {'SET'},
|
||||
'tr': {'IST'},
|
||||
'tw': {'TAI', 'TWO'},
|
||||
@@ -393,6 +464,7 @@ EQUITY_SCREENER_EQ_MAP = {
|
||||
"Real Estate", "Technology", "Energy", "Utilities", "Financial Services",
|
||||
"Consumer Defensive", "Consumer Cyclical"
|
||||
},
|
||||
"industry": SECTOR_INDUSTY_MAPPING,
|
||||
"peer_group": {
|
||||
"US Fund Equity Energy",
|
||||
"US CE Convertibles",
|
||||
@@ -529,7 +601,8 @@ EQUITY_SCREENER_FIELDS = {
|
||||
"eq_fields": {
|
||||
"region",
|
||||
"sector",
|
||||
"peer_group"},
|
||||
"peer_group",
|
||||
"industry"},
|
||||
"price":{
|
||||
"lastclosemarketcap.lasttwelvemonths",
|
||||
"percentchange",
|
||||
|
||||
257
yfinance/data.py
257
yfinance/data.py
@@ -1,8 +1,8 @@
|
||||
import functools
|
||||
import random
|
||||
from functools import lru_cache
|
||||
|
||||
from curl_cffi import requests
|
||||
from urllib.parse import urlsplit, urljoin
|
||||
from bs4 import BeautifulSoup
|
||||
import datetime
|
||||
|
||||
@@ -11,8 +11,7 @@ from frozendict import frozendict
|
||||
from . import utils, cache
|
||||
import threading
|
||||
|
||||
from .const import USER_AGENTS
|
||||
from .exceptions import YFRateLimitError
|
||||
from .exceptions import YFRateLimitError, YFDataException
|
||||
|
||||
cache_maxsize = 64
|
||||
|
||||
@@ -66,9 +65,6 @@ class YfData(metaclass=SingletonMeta):
|
||||
Have one place to retrieve data from Yahoo API in order to ease caching and speed up operations.
|
||||
Singleton means one session one cookie shared by all threads.
|
||||
"""
|
||||
user_agent_headers = {
|
||||
'User-Agent': random.choice(USER_AGENTS)
|
||||
}
|
||||
|
||||
def __init__(self, session=None, proxy=None):
|
||||
self._crumb = None
|
||||
@@ -85,18 +81,12 @@ class YfData(metaclass=SingletonMeta):
|
||||
self._set_session(session or requests.Session(impersonate="chrome"))
|
||||
self._set_proxy(proxy)
|
||||
|
||||
utils.get_yf_logger().debug(f"Using User-Agent: {self.user_agent_headers['User-Agent']}")
|
||||
|
||||
def _set_session(self, session):
|
||||
if session is None:
|
||||
return
|
||||
with self._cookie_lock:
|
||||
self._session = session
|
||||
if self._proxy is not None:
|
||||
self._session.proxies = self._proxy
|
||||
|
||||
try:
|
||||
self._session.cache
|
||||
session.cache
|
||||
except AttributeError:
|
||||
# Not caching
|
||||
self._session_is_caching = False
|
||||
@@ -105,8 +95,16 @@ class YfData(metaclass=SingletonMeta):
|
||||
# Can't simply use a non-caching session to fetch cookie & crumb,
|
||||
# because then the caching-session won't have cookie.
|
||||
self._session_is_caching = True
|
||||
from requests_cache import DO_NOT_CACHE
|
||||
self._expire_after = DO_NOT_CACHE
|
||||
# But since switch to curl_cffi, can't use requests_cache with it.
|
||||
raise YFDataException("request_cache sessions don't work with curl_cffi, which is necessary now for Yahoo API. Solution: stop setting session, let YF handle.")
|
||||
|
||||
if not isinstance(session, requests.session.Session):
|
||||
raise YFDataException(f"Yahoo API requires curl_cffi session not {type(session)}. Solution: stop setting session, let YF handle.")
|
||||
|
||||
with self._cookie_lock:
|
||||
self._session = session
|
||||
if self._proxy is not None:
|
||||
self._session.proxies = self._proxy
|
||||
|
||||
def _set_proxy(self, proxy=None):
|
||||
with self._cookie_lock:
|
||||
@@ -140,58 +138,83 @@ class YfData(metaclass=SingletonMeta):
|
||||
if not have_lock:
|
||||
self._cookie_lock.release()
|
||||
|
||||
def _save_session_cookies(self):
|
||||
try:
|
||||
cache.get_cookie_cache().store('csrf', self._session.cookies)
|
||||
except Exception:
|
||||
@utils.log_indent_decorator
|
||||
def _save_cookie_curlCffi(self):
|
||||
if self._session is None:
|
||||
return False
|
||||
cookies = self._session.cookies.jar._cookies
|
||||
if len(cookies) == 0:
|
||||
return False
|
||||
yh_domains = [k for k in cookies.keys() if 'yahoo' in k]
|
||||
if len(yh_domains) > 1:
|
||||
# Possible when cookie fetched with CSRF method. Discard consent cookie.
|
||||
yh_domains = [k for k in yh_domains if 'consent' not in k]
|
||||
if len(yh_domains) > 1:
|
||||
utils.get_yf_logger().debug(f'Multiple Yahoo cookies, not sure which to cache: {yh_domains}')
|
||||
return False
|
||||
if len(yh_domains) == 0:
|
||||
return False
|
||||
yh_domain = yh_domains[0]
|
||||
yh_cookie = {yh_domain: cookies[yh_domain]}
|
||||
cache.get_cookie_cache().store('curlCffi', yh_cookie)
|
||||
return True
|
||||
|
||||
def _load_session_cookies(self):
|
||||
cookie_dict = cache.get_cookie_cache().lookup('csrf')
|
||||
if cookie_dict is None:
|
||||
@utils.log_indent_decorator
|
||||
def _load_cookie_curlCffi(self):
|
||||
if self._session is None:
|
||||
return False
|
||||
# Periodically refresh, 24 hours seems fair.
|
||||
if cookie_dict['age'] > datetime.timedelta(days=1):
|
||||
cookie_dict = cache.get_cookie_cache().lookup('curlCffi')
|
||||
if cookie_dict is None or len(cookie_dict) == 0:
|
||||
return False
|
||||
self._session.cookies.update(cookie_dict['cookie'])
|
||||
utils.get_yf_logger().debug('loaded persistent cookie')
|
||||
|
||||
def _save_cookie_basic(self, cookie):
|
||||
try:
|
||||
cache.get_cookie_cache().store('basic', cookie)
|
||||
except Exception:
|
||||
cookies = cookie_dict['cookie']
|
||||
domain = list(cookies.keys())[0]
|
||||
cookie = cookies[domain]['/']['A3']
|
||||
expiry_ts = cookie.expires
|
||||
if expiry_ts > 2e9:
|
||||
# convert ms to s
|
||||
expiry_ts //= 1e3
|
||||
expiry_dt = datetime.datetime.fromtimestamp(expiry_ts, tz=datetime.timezone.utc)
|
||||
expired = expiry_dt < datetime.datetime.now(datetime.timezone.utc)
|
||||
if expired:
|
||||
utils.get_yf_logger().debug('cached cookie expired')
|
||||
return False
|
||||
self._session.cookies.jar._cookies.update(cookies)
|
||||
self._cookie = cookie
|
||||
return True
|
||||
def _load_cookie_basic(self):
|
||||
cookie_dict = cache.get_cookie_cache().lookup('basic')
|
||||
if cookie_dict is None:
|
||||
return None
|
||||
# Periodically refresh, 24 hours seems fair.
|
||||
if cookie_dict['age'] > datetime.timedelta(days=1):
|
||||
return None
|
||||
utils.get_yf_logger().debug('loaded persistent cookie')
|
||||
return cookie_dict['cookie']
|
||||
|
||||
@utils.log_indent_decorator
|
||||
def _get_cookie_basic(self, timeout=30):
|
||||
if self._cookie is not None:
|
||||
utils.get_yf_logger().debug('reusing cookie')
|
||||
return True
|
||||
elif self._load_cookie_curlCffi():
|
||||
utils.get_yf_logger().debug('reusing persistent cookie')
|
||||
return True
|
||||
|
||||
# To avoid infinite recursion, do NOT use self.get()
|
||||
# - 'allow_redirects' copied from @psychoz971 solution - does it help USA?
|
||||
self._session.get(
|
||||
url='https://fc.yahoo.com',
|
||||
headers=self.user_agent_headers,
|
||||
timeout=timeout,
|
||||
allow_redirects=True)
|
||||
try:
|
||||
self._session.get(
|
||||
url='https://fc.yahoo.com',
|
||||
timeout=timeout,
|
||||
allow_redirects=True)
|
||||
except requests.exceptions.DNSError:
|
||||
# Possible because url on some privacy/ad blocklists
|
||||
return False
|
||||
self._save_cookie_curlCffi()
|
||||
return True
|
||||
|
||||
@utils.log_indent_decorator
|
||||
def _get_crumb_basic(self, timeout=30):
|
||||
if self._crumb is not None:
|
||||
utils.get_yf_logger().debug('reusing crumb')
|
||||
return self._crumb
|
||||
|
||||
self._get_cookie_basic()
|
||||
if not self._get_cookie_basic():
|
||||
return None
|
||||
# - 'allow_redirects' copied from @psychoz971 solution - does it help USA?
|
||||
get_args = {
|
||||
'url': "https://query1.finance.yahoo.com/v1/test/getcrumb",
|
||||
'headers': self.user_agent_headers,
|
||||
'timeout': timeout,
|
||||
'allow_redirects': True
|
||||
}
|
||||
@@ -201,6 +224,10 @@ class YfData(metaclass=SingletonMeta):
|
||||
else:
|
||||
crumb_response = self._session.get(**get_args)
|
||||
self._crumb = crumb_response.text
|
||||
if crumb_response.status_code == 429 or "Too Many Requests" in self._crumb:
|
||||
utils.get_yf_logger().debug(f"Didn't receive crumb {self._crumb}")
|
||||
raise YFRateLimitError()
|
||||
|
||||
if self._crumb is None or '<html>' in self._crumb:
|
||||
utils.get_yf_logger().debug("Didn't receive crumb")
|
||||
return None
|
||||
@@ -210,22 +237,22 @@ class YfData(metaclass=SingletonMeta):
|
||||
|
||||
@utils.log_indent_decorator
|
||||
def _get_cookie_and_crumb_basic(self, timeout):
|
||||
self._get_cookie_basic(timeout)
|
||||
crumb = self._get_crumb_basic(timeout)
|
||||
return crumb
|
||||
if not self._get_cookie_basic(timeout):
|
||||
return None
|
||||
return self._get_crumb_basic(timeout)
|
||||
|
||||
@utils.log_indent_decorator
|
||||
def _get_cookie_csrf(self, timeout):
|
||||
if self._cookie is not None:
|
||||
utils.get_yf_logger().debug('reusing cookie')
|
||||
return True
|
||||
|
||||
elif self._load_session_cookies():
|
||||
elif self._load_cookie_curlCffi():
|
||||
utils.get_yf_logger().debug('reusing persistent cookie')
|
||||
self._cookie = True
|
||||
return True
|
||||
|
||||
base_args = {
|
||||
'headers': self.user_agent_headers,
|
||||
'timeout': timeout}
|
||||
|
||||
get_args = {**base_args, 'url': 'https://guce.yahoo.com/consent'}
|
||||
@@ -280,7 +307,7 @@ class YfData(metaclass=SingletonMeta):
|
||||
# No idea why happens, but handle nicely so can switch to other cookie method.
|
||||
utils.get_yf_logger().debug('_get_cookie_csrf() encountering requests.exceptions.ChunkedEncodingError, aborting')
|
||||
self._cookie = True
|
||||
self._save_session_cookies()
|
||||
self._save_cookie_curlCffi()
|
||||
return True
|
||||
|
||||
@utils.log_indent_decorator
|
||||
@@ -297,7 +324,6 @@ class YfData(metaclass=SingletonMeta):
|
||||
|
||||
get_args = {
|
||||
'url': 'https://query2.finance.yahoo.com/v1/test/getcrumb',
|
||||
'headers': self.user_agent_headers,
|
||||
'timeout': timeout}
|
||||
if self._session_is_caching:
|
||||
get_args['expire_after'] = self._expire_after
|
||||
@@ -306,6 +332,10 @@ class YfData(metaclass=SingletonMeta):
|
||||
r = self._session.get(**get_args)
|
||||
self._crumb = r.text
|
||||
|
||||
if r.status_code == 429 or "Too Many Requests" in self._crumb:
|
||||
utils.get_yf_logger().debug(f"Didn't receive crumb {self._crumb}")
|
||||
raise YFRateLimitError()
|
||||
|
||||
if self._crumb is None or '<html>' in self._crumb or self._crumb == '':
|
||||
utils.get_yf_logger().debug("Didn't receive crumb")
|
||||
return None
|
||||
@@ -337,15 +367,25 @@ class YfData(metaclass=SingletonMeta):
|
||||
return crumb, strategy
|
||||
|
||||
@utils.log_indent_decorator
|
||||
def get(self, url, user_agent_headers=None, params=None, timeout=30):
|
||||
return self._make_request(url, request_method = self._session.get, user_agent_headers=user_agent_headers, params=params, timeout=timeout)
|
||||
|
||||
def get(self, url, params=None, timeout=30):
|
||||
response = self._make_request(url, request_method = self._session.get, params=params, timeout=timeout)
|
||||
|
||||
# Accept cookie-consent if redirected to consent page
|
||||
if not self._is_this_consent_url(response.url):
|
||||
# "Consent Page not detected"
|
||||
pass
|
||||
else:
|
||||
# "Consent Page detected"
|
||||
response = self._accept_consent_form(response, timeout)
|
||||
|
||||
return response
|
||||
|
||||
@utils.log_indent_decorator
|
||||
def post(self, url, body, user_agent_headers=None, params=None, timeout=30):
|
||||
return self._make_request(url, request_method = self._session.post, user_agent_headers=user_agent_headers, body=body, params=params, timeout=timeout)
|
||||
|
||||
def post(self, url, body, params=None, timeout=30):
|
||||
return self._make_request(url, request_method = self._session.post, body=body, params=params, timeout=timeout)
|
||||
|
||||
@utils.log_indent_decorator
|
||||
def _make_request(self, url, request_method, user_agent_headers=None, body=None, params=None, timeout=30):
|
||||
def _make_request(self, url, request_method, body=None, params=None, timeout=30):
|
||||
# Important: treat input arguments as immutable.
|
||||
|
||||
if len(url) > 200:
|
||||
@@ -368,13 +408,12 @@ class YfData(metaclass=SingletonMeta):
|
||||
request_args = {
|
||||
'url': url,
|
||||
'params': {**params, **crumbs},
|
||||
'timeout': timeout,
|
||||
'headers': user_agent_headers or self.user_agent_headers
|
||||
'timeout': timeout
|
||||
}
|
||||
|
||||
if body:
|
||||
request_args['json'] = body
|
||||
|
||||
|
||||
response = request_method(**request_args)
|
||||
utils.get_yf_logger().debug(f'response code={response.status_code}')
|
||||
if response.status_code >= 400:
|
||||
@@ -396,11 +435,91 @@ class YfData(metaclass=SingletonMeta):
|
||||
|
||||
@lru_cache_freezeargs
|
||||
@lru_cache(maxsize=cache_maxsize)
|
||||
def cache_get(self, url, user_agent_headers=None, params=None, timeout=30):
|
||||
return self.get(url, user_agent_headers, params, timeout)
|
||||
def cache_get(self, url, params=None, timeout=30):
|
||||
return self.get(url, params, timeout)
|
||||
|
||||
def get_raw_json(self, url, user_agent_headers=None, params=None, timeout=30):
|
||||
def get_raw_json(self, url, params=None, timeout=30):
|
||||
utils.get_yf_logger().debug(f'get_raw_json(): {url}')
|
||||
response = self.get(url, user_agent_headers=user_agent_headers, params=params, timeout=timeout)
|
||||
response = self.get(url, params=params, timeout=timeout)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
return response.json()
|
||||
|
||||
def _is_this_consent_url(self, response_url: str) -> bool:
|
||||
"""
|
||||
Check if given response_url is consent page
|
||||
|
||||
Args:
|
||||
response_url (str) : response.url
|
||||
|
||||
Returns:
|
||||
True : This is cookie-consent page
|
||||
False : This is not cookie-consent page
|
||||
"""
|
||||
try:
|
||||
return urlsplit(response_url).hostname and urlsplit(
|
||||
response_url
|
||||
).hostname.endswith("consent.yahoo.com")
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _accept_consent_form(
|
||||
self, consent_resp: requests.Response, timeout: int
|
||||
) -> requests.Response:
|
||||
"""
|
||||
Click 'Accept all' to cookie-consent form and return response object.
|
||||
|
||||
Args:
|
||||
consent_resp (requests.Response) : Response instance of cookie-consent page
|
||||
timeout (int) : Raise TimeoutError if post doesn't respond
|
||||
|
||||
Returns:
|
||||
response (requests.Response) : Reponse instance received from the server after accepting cookie-consent post.
|
||||
"""
|
||||
soup = BeautifulSoup(consent_resp.text, "html.parser")
|
||||
|
||||
# Heuristic: pick the first form; Yahoo's CMP tends to have a single form for consent
|
||||
form = soup.find("form")
|
||||
if not form:
|
||||
return consent_resp
|
||||
|
||||
# action : URL to send "Accept Cookies"
|
||||
action = form.get("action") or consent_resp.url
|
||||
action = urljoin(consent_resp.url, action)
|
||||
|
||||
# Collect inputs (hidden tokens, etc.)
|
||||
"""
|
||||
<input name="csrfToken" type="hidden" value="..."/>
|
||||
<input name="sessionId" type="hidden" value="..."/>
|
||||
<input name="originalDoneUrl" type="hidden" value="..."/>
|
||||
<input name="namespace" type="hidden" value="yahoo"/>
|
||||
"""
|
||||
data = {}
|
||||
for inp in form.find_all("input"):
|
||||
name = inp.get("name")
|
||||
if not name:
|
||||
continue
|
||||
typ = (inp.get("type") or "text").lower()
|
||||
val = inp.get("value") or ""
|
||||
|
||||
if typ in ("checkbox", "radio"):
|
||||
# If it's clearly an "agree"/"accept" field or already checked, include it
|
||||
if (
|
||||
"agree" in name.lower()
|
||||
or "accept" in name.lower()
|
||||
or inp.has_attr("checked")
|
||||
):
|
||||
data[name] = val if val != "" else "1"
|
||||
else:
|
||||
data[name] = val
|
||||
|
||||
# If no explicit agree/accept in inputs, add a best-effort flag
|
||||
lowered = {k.lower() for k in data.keys()}
|
||||
if not any(("agree" in k or "accept" in k) for k in lowered):
|
||||
data["agree"] = "1"
|
||||
|
||||
# Submit the form with "Referer". Some servers check this header as a simple CSRF protection measure.
|
||||
headers = {"Referer": consent_resp.url}
|
||||
response = self._session.post(
|
||||
action, data=data, headers=headers, timeout=timeout, allow_redirects=True
|
||||
)
|
||||
return response
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from ..ticker import Ticker
|
||||
import pandas as _pd
|
||||
from typing import Dict, List, Optional
|
||||
import warnings
|
||||
|
||||
from ..const import _QUERY1_URL_, _SENTINEL_
|
||||
from ..data import YfData
|
||||
from ..utils import print_once
|
||||
from typing import Dict, List, Optional
|
||||
import pandas as _pd
|
||||
from ..ticker import Ticker
|
||||
|
||||
_QUERY_URL_ = f'{_QUERY1_URL_}/v1/finance'
|
||||
|
||||
@@ -26,7 +27,7 @@ class Domain(ABC):
|
||||
self.session = session
|
||||
self._data: YfData = YfData(session=session)
|
||||
if proxy is not _SENTINEL_:
|
||||
print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
self._name: Optional[str] = None
|
||||
@@ -122,7 +123,7 @@ class Domain(ABC):
|
||||
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)
|
||||
result = self._data.get_raw_json(query_url, params=params_dict)
|
||||
return result
|
||||
|
||||
def _parse_and_assign_common(self, data) -> None:
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
from __future__ import print_function
|
||||
from typing import Dict, Optional
|
||||
|
||||
import pandas as _pd
|
||||
from typing import Dict, Optional
|
||||
import warnings
|
||||
|
||||
from .. import utils
|
||||
from ..const import _SENTINEL_
|
||||
from ..data import YfData
|
||||
|
||||
from .domain import Domain, _QUERY_URL_
|
||||
from .. import utils
|
||||
from ..data import YfData
|
||||
from ..const import _SENTINEL_
|
||||
|
||||
class Industry(Domain):
|
||||
"""
|
||||
@@ -20,8 +22,9 @@ class Industry(Domain):
|
||||
session (optional): The session to use for requests.
|
||||
"""
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
YfData(session=session, proxy=proxy)
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
YfData(proxy=proxy)
|
||||
YfData(session=session)
|
||||
super(Industry, self).__init__(key, session)
|
||||
self._query_url = f'{_QUERY_URL_}/industries/{self._key}'
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import datetime as dt
|
||||
|
||||
from ..data import YfData
|
||||
from ..data import utils
|
||||
from ..const import _QUERY1_URL_, _SENTINEL_
|
||||
import json as _json
|
||||
import warnings
|
||||
|
||||
from ..const import _QUERY1_URL_, _SENTINEL_
|
||||
from ..data import utils, YfData
|
||||
|
||||
class Market:
|
||||
def __init__(self, market:'str', session=None, proxy=_SENTINEL_, timeout=30):
|
||||
@@ -13,7 +13,7 @@ class Market:
|
||||
|
||||
self._data = YfData(session=self.session)
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
self._logger = utils.get_yf_logger()
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
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, _SENTINEL_
|
||||
|
||||
import pandas as _pd
|
||||
from typing import Dict, Optional
|
||||
import warnings
|
||||
|
||||
from ..const import SECTOR_INDUSTY_MAPPING_LC, _SENTINEL_
|
||||
from ..data import YfData
|
||||
from ..utils import dynamic_docstring, generate_list_table_from_dict, get_yf_logger
|
||||
|
||||
from .domain import Domain, _QUERY_URL_
|
||||
from .. import utils
|
||||
from ..data import YfData
|
||||
|
||||
class Sector(Domain):
|
||||
"""
|
||||
@@ -28,7 +29,7 @@ class Sector(Domain):
|
||||
Map of sector and industry
|
||||
"""
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
YfData(session=session, proxy=proxy)
|
||||
|
||||
super(Sector, self).__init__(key, session)
|
||||
@@ -68,7 +69,7 @@ class Sector(Domain):
|
||||
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)})
|
||||
@dynamic_docstring({"sector_industry": generate_list_table_from_dict(SECTOR_INDUSTY_MAPPING_LC,bullets=True)})
|
||||
@property
|
||||
def industries(self) -> _pd.DataFrame:
|
||||
"""
|
||||
@@ -147,7 +148,7 @@ class Sector(Domain):
|
||||
self._industries = self._parse_industries(data.get('industries', {}))
|
||||
|
||||
except Exception as e:
|
||||
logger = utils.get_yf_logger()
|
||||
logger = get_yf_logger()
|
||||
logger.error(f"Failed to get sector data for '{self._key}' reason: {e}")
|
||||
logger.debug("Got response: ")
|
||||
logger.debug("-------------")
|
||||
|
||||
@@ -45,7 +45,7 @@ class YFInvalidPeriodError(YFException):
|
||||
self.invalid_period = invalid_period
|
||||
self.valid_ranges = valid_ranges
|
||||
super().__init__(f"{self.ticker}: Period '{invalid_period}' is invalid, "
|
||||
f"must be of the format {valid_ranges}, etc.")
|
||||
f"must be one of: {valid_ranges}")
|
||||
|
||||
|
||||
class YFRateLimitError(YFException):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import asyncio
|
||||
import base64
|
||||
import json
|
||||
from typing import List, Optional, Callable
|
||||
from typing import List, Optional, Callable, Union
|
||||
|
||||
from websockets.sync.client import connect as sync_connect
|
||||
from websockets.asyncio.client import connect as async_connect
|
||||
@@ -84,12 +84,12 @@ class AsyncWebSocket(BaseWebSocket):
|
||||
print(f"Error in heartbeat subscription: {e}")
|
||||
break
|
||||
|
||||
async def subscribe(self, symbols: str | List[str]):
|
||||
async def subscribe(self, symbols: Union[str, List[str]]):
|
||||
"""
|
||||
Subscribe to a stock symbol or a list of stock symbols.
|
||||
|
||||
Args:
|
||||
symbols (str | List[str]): Stock symbol(s) to subscribe to.
|
||||
symbols (Union[str, List[str]]): Stock symbol(s) to subscribe to.
|
||||
"""
|
||||
await self._connect()
|
||||
|
||||
@@ -109,12 +109,12 @@ class AsyncWebSocket(BaseWebSocket):
|
||||
if self.verbose:
|
||||
print(f"Subscribed to symbols: {symbols}")
|
||||
|
||||
async def unsubscribe(self, symbols: str | List[str]):
|
||||
async def unsubscribe(self, symbols: Union[str, List[str]]):
|
||||
"""
|
||||
Unsubscribe from a stock symbol or a list of stock symbols.
|
||||
|
||||
Args:
|
||||
symbols (str | List[str]): Stock symbol(s) to unsubscribe from.
|
||||
symbols (Union[str, List[str]]): Stock symbol(s) to unsubscribe from.
|
||||
"""
|
||||
await self._connect()
|
||||
|
||||
@@ -235,12 +235,12 @@ class WebSocket(BaseWebSocket):
|
||||
self._ws = None
|
||||
raise
|
||||
|
||||
def subscribe(self, symbols: str | List[str]):
|
||||
def subscribe(self, symbols: Union[str, List[str]]):
|
||||
"""
|
||||
Subscribe to a stock symbol or a list of stock symbols.
|
||||
|
||||
Args:
|
||||
symbols (str | List[str]): Stock symbol(s) to subscribe to.
|
||||
symbols (Union[str, List[str]]): Stock symbol(s) to subscribe to.
|
||||
"""
|
||||
self._connect()
|
||||
|
||||
@@ -256,12 +256,12 @@ class WebSocket(BaseWebSocket):
|
||||
if self.verbose:
|
||||
print(f"Subscribed to symbols: {symbols}")
|
||||
|
||||
def unsubscribe(self, symbols: str | List[str]):
|
||||
def unsubscribe(self, symbols: Union[str, List[str]]):
|
||||
"""
|
||||
Unsubscribe from a stock symbol or a list of stock symbols.
|
||||
|
||||
Args:
|
||||
symbols (str | List[str]): Stock symbol(s) to unsubscribe from.
|
||||
symbols (Union[str, List[str]]): Stock symbol(s) to unsubscribe from.
|
||||
"""
|
||||
self._connect()
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
#
|
||||
|
||||
import json as _json
|
||||
|
||||
import pandas as pd
|
||||
import warnings
|
||||
|
||||
from . import utils
|
||||
from .const import _QUERY1_URL_, _SENTINEL_
|
||||
@@ -43,12 +43,12 @@ class Lookup:
|
||||
:param raise_errors: Raise exceptions on error (default True).
|
||||
"""
|
||||
|
||||
def __init__(self, query: str, session=None, proxy=None, timeout=30, raise_errors=True):
|
||||
def __init__(self, query: str, session=None, proxy=_SENTINEL_, timeout=30, raise_errors=True):
|
||||
self.session = session
|
||||
self._data = YfData(session=self.session)
|
||||
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
self.query = query
|
||||
|
||||
@@ -25,6 +25,7 @@ import logging
|
||||
import time as _time
|
||||
import traceback
|
||||
from typing import Union
|
||||
import warnings
|
||||
|
||||
import multitasking as _multitasking
|
||||
import pandas as _pd
|
||||
@@ -38,7 +39,7 @@ from .const import _SENTINEL_
|
||||
@utils.log_indent_decorator
|
||||
def download(tickers, start=None, end=None, actions=False, threads=True,
|
||||
ignore_tz=None, group_by='column', auto_adjust=None, back_adjust=False,
|
||||
repair=False, keepna=False, progress=True, period="max", interval="1d",
|
||||
repair=False, keepna=False, progress=True, period=None, interval="1d",
|
||||
prepost=False, proxy=_SENTINEL_, rounding=False, timeout=10, session=None,
|
||||
multi_level_index=True) -> Union[_pd.DataFrame, None]:
|
||||
"""
|
||||
@@ -48,6 +49,7 @@ def download(tickers, start=None, end=None, actions=False, threads=True,
|
||||
List of tickers to download
|
||||
period : str
|
||||
Valid periods: 1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max
|
||||
Default: 1mo
|
||||
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
|
||||
@@ -93,9 +95,15 @@ def download(tickers, start=None, end=None, actions=False, threads=True,
|
||||
logger = utils.get_yf_logger()
|
||||
session = session or requests.Session(impersonate="chrome")
|
||||
|
||||
# Ensure data initialised with session.
|
||||
if proxy is not _SENTINEL_:
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=3)
|
||||
YfData(proxy=proxy)
|
||||
YfData(session=session)
|
||||
|
||||
if auto_adjust is None:
|
||||
# Warn users that default has changed to True
|
||||
utils.print_once("YF.download() has changed argument auto_adjust default to True")
|
||||
warnings.warn("YF.download() has changed argument auto_adjust default to True", FutureWarning, stacklevel=3)
|
||||
auto_adjust = True
|
||||
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
@@ -127,7 +135,7 @@ def download(tickers, start=None, end=None, actions=False, threads=True,
|
||||
for ticker in tickers:
|
||||
if utils.is_isin(ticker):
|
||||
isin = ticker
|
||||
ticker = utils.get_ticker_by_isin(ticker, session=session)
|
||||
ticker = utils.get_ticker_by_isin(ticker)
|
||||
shared._ISINS[ticker] = isin
|
||||
_tickers_.append(ticker)
|
||||
|
||||
@@ -143,13 +151,6 @@ def download(tickers, start=None, end=None, actions=False, threads=True,
|
||||
shared._ERRORS = {}
|
||||
shared._TRACEBACKS = {}
|
||||
|
||||
# Ensure data initialised with session.
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
YfData(session=session, proxy=proxy)
|
||||
else:
|
||||
YfData(session=session)
|
||||
|
||||
# download using threads
|
||||
if threads:
|
||||
if threads is True:
|
||||
|
||||
@@ -1,22 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# NO CHECKED-IN PROTOBUF GENCODE
|
||||
# source: yfinance/pricing.proto
|
||||
# Protobuf Python Version: 5.29.0
|
||||
# source: pricing.proto
|
||||
"""Generated protocol buffer code."""
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||
from google.protobuf import runtime_version as _runtime_version
|
||||
from google.protobuf import message as _message
|
||||
from google.protobuf import reflection as _reflection
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
from google.protobuf.internal import builder as _builder
|
||||
_runtime_version.ValidateProtobufRuntimeVersion(
|
||||
_runtime_version.Domain.PUBLIC,
|
||||
5,
|
||||
29,
|
||||
0,
|
||||
'',
|
||||
'yfinance/pricing.proto'
|
||||
)
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
@@ -24,13 +14,20 @@ _sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16yfinance/pricing.proto\"\x9a\x05\n\x0bPricingData\x12\n\n\x02id\x18\x01 \x01(\t\x12\r\n\x05price\x18\x02 \x01(\x02\x12\x0c\n\x04time\x18\x03 \x01(\x12\x12\x10\n\x08\x63urrency\x18\x04 \x01(\t\x12\x10\n\x08\x65xchange\x18\x05 \x01(\t\x12\x12\n\nquote_type\x18\x06 \x01(\x05\x12\x14\n\x0cmarket_hours\x18\x07 \x01(\x05\x12\x16\n\x0e\x63hange_percent\x18\x08 \x01(\x02\x12\x12\n\nday_volume\x18\t \x01(\x12\x12\x10\n\x08\x64\x61y_high\x18\n \x01(\x02\x12\x0f\n\x07\x64\x61y_low\x18\x0b \x01(\x02\x12\x0e\n\x06\x63hange\x18\x0c \x01(\x02\x12\x12\n\nshort_name\x18\r \x01(\t\x12\x13\n\x0b\x65xpire_date\x18\x0e \x01(\x12\x12\x12\n\nopen_price\x18\x0f \x01(\x02\x12\x16\n\x0eprevious_close\x18\x10 \x01(\x02\x12\x14\n\x0cstrike_price\x18\x11 \x01(\x02\x12\x19\n\x11underlying_symbol\x18\x12 \x01(\t\x12\x15\n\ropen_interest\x18\x13 \x01(\x12\x12\x14\n\x0coptions_type\x18\x14 \x01(\x12\x12\x13\n\x0bmini_option\x18\x15 \x01(\x12\x12\x11\n\tlast_size\x18\x16 \x01(\x12\x12\x0b\n\x03\x62id\x18\x17 \x01(\x02\x12\x10\n\x08\x62id_size\x18\x18 \x01(\x12\x12\x0b\n\x03\x61sk\x18\x19 \x01(\x02\x12\x10\n\x08\x61sk_size\x18\x1a \x01(\x12\x12\x12\n\nprice_hint\x18\x1b \x01(\x12\x12\x10\n\x08vol_24hr\x18\x1c \x01(\x12\x12\x1a\n\x12vol_all_currencies\x18\x1d \x01(\x12\x12\x15\n\rfrom_currency\x18\x1e \x01(\t\x12\x13\n\x0blast_market\x18\x1f \x01(\t\x12\x1a\n\x12\x63irculating_supply\x18 \x01(\x01\x12\x12\n\nmarket_cap\x18! \x01(\x01\x62\x06proto3')
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\rpricing.proto\"\x9a\x05\n\x0bPricingData\x12\n\n\x02id\x18\x01 \x01(\t\x12\r\n\x05price\x18\x02 \x01(\x02\x12\x0c\n\x04time\x18\x03 \x01(\x12\x12\x10\n\x08\x63urrency\x18\x04 \x01(\t\x12\x10\n\x08\x65xchange\x18\x05 \x01(\t\x12\x12\n\nquote_type\x18\x06 \x01(\x05\x12\x14\n\x0cmarket_hours\x18\x07 \x01(\x05\x12\x16\n\x0e\x63hange_percent\x18\x08 \x01(\x02\x12\x12\n\nday_volume\x18\t \x01(\x12\x12\x10\n\x08\x64\x61y_high\x18\n \x01(\x02\x12\x0f\n\x07\x64\x61y_low\x18\x0b \x01(\x02\x12\x0e\n\x06\x63hange\x18\x0c \x01(\x02\x12\x12\n\nshort_name\x18\r \x01(\t\x12\x13\n\x0b\x65xpire_date\x18\x0e \x01(\x12\x12\x12\n\nopen_price\x18\x0f \x01(\x02\x12\x16\n\x0eprevious_close\x18\x10 \x01(\x02\x12\x14\n\x0cstrike_price\x18\x11 \x01(\x02\x12\x19\n\x11underlying_symbol\x18\x12 \x01(\t\x12\x15\n\ropen_interest\x18\x13 \x01(\x12\x12\x14\n\x0coptions_type\x18\x14 \x01(\x12\x12\x13\n\x0bmini_option\x18\x15 \x01(\x12\x12\x11\n\tlast_size\x18\x16 \x01(\x12\x12\x0b\n\x03\x62id\x18\x17 \x01(\x02\x12\x10\n\x08\x62id_size\x18\x18 \x01(\x12\x12\x0b\n\x03\x61sk\x18\x19 \x01(\x02\x12\x10\n\x08\x61sk_size\x18\x1a \x01(\x12\x12\x12\n\nprice_hint\x18\x1b \x01(\x12\x12\x10\n\x08vol_24hr\x18\x1c \x01(\x12\x12\x1a\n\x12vol_all_currencies\x18\x1d \x01(\x12\x12\x15\n\rfrom_currency\x18\x1e \x01(\t\x12\x13\n\x0blast_market\x18\x1f \x01(\t\x12\x1a\n\x12\x63irculating_supply\x18 \x01(\x01\x12\x12\n\nmarket_cap\x18! \x01(\x01\x62\x06proto3')
|
||||
|
||||
|
||||
|
||||
_PRICINGDATA = DESCRIPTOR.message_types_by_name['PricingData']
|
||||
PricingData = _reflection.GeneratedProtocolMessageType('PricingData', (_message.Message,), {
|
||||
'DESCRIPTOR' : _PRICINGDATA,
|
||||
'__module__' : 'pricing_pb2'
|
||||
# @@protoc_insertion_point(class_scope:PricingData)
|
||||
})
|
||||
_sym_db.RegisterMessage(PricingData)
|
||||
|
||||
_globals = globals()
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'yfinance.pricing_pb2', _globals)
|
||||
if not _descriptor._USE_C_DESCRIPTORS:
|
||||
DESCRIPTOR._loaded_options = None
|
||||
_globals['_PRICINGDATA']._serialized_start=27
|
||||
_globals['_PRICINGDATA']._serialized_end=693
|
||||
DESCRIPTOR._options = None
|
||||
_PRICINGDATA._serialized_start=18
|
||||
_PRICINGDATA._serialized_end=684
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import curl_cffi
|
||||
import pandas as pd
|
||||
import requests
|
||||
import warnings
|
||||
|
||||
from yfinance import utils
|
||||
from yfinance.data import YfData
|
||||
from yfinance.const import quote_summary_valid_modules, _SENTINEL_
|
||||
from yfinance.scrapers.quote import _QUOTE_SUMMARY_URL_
|
||||
from yfinance.data import YfData
|
||||
from yfinance.exceptions import YFException
|
||||
from yfinance.scrapers.quote import _QUOTE_SUMMARY_URL_
|
||||
|
||||
class Analysis:
|
||||
|
||||
def __init__(self, data: YfData, symbol: str, proxy=_SENTINEL_):
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
data._set_proxy(proxy)
|
||||
|
||||
self._data = data
|
||||
@@ -177,9 +178,9 @@ class Analysis:
|
||||
raise YFException("No valid modules provided, see available modules using `valid_modules`")
|
||||
params_dict = {"modules": modules, "corsDomain": "finance.yahoo.com", "formatted": "false", "symbol": self._symbol}
|
||||
try:
|
||||
result = self._data.get_raw_json(_QUOTE_SUMMARY_URL_ + f"/{self._symbol}", user_agent_headers=self._data.user_agent_headers, params=params_dict)
|
||||
except requests.exceptions.HTTPError as e:
|
||||
utils.get_yf_logger().error(str(e))
|
||||
result = self._data.get_raw_json(_QUOTE_SUMMARY_URL_ + f"/{self._symbol}", params=params_dict)
|
||||
except curl_cffi.requests.exceptions.HTTPError as e:
|
||||
utils.get_yf_logger().error(str(e) + e.response.text)
|
||||
return None
|
||||
return result
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ class Fundamentals:
|
||||
|
||||
def __init__(self, data: YfData, symbol: str, proxy=const._SENTINEL_):
|
||||
if proxy is not const._SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
data._set_proxy(proxy)
|
||||
|
||||
self._data = data
|
||||
@@ -53,7 +53,7 @@ class Financials:
|
||||
|
||||
def get_income_time_series(self, freq="yearly", proxy=const._SENTINEL_) -> pd.DataFrame:
|
||||
if proxy is not const._SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
res = self._income_time_series
|
||||
@@ -63,7 +63,7 @@ class Financials:
|
||||
|
||||
def get_balance_sheet_time_series(self, freq="yearly", proxy=const._SENTINEL_) -> pd.DataFrame:
|
||||
if proxy is not const._SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
res = self._balance_sheet_time_series
|
||||
@@ -73,7 +73,7 @@ class Financials:
|
||||
|
||||
def get_cash_flow_time_series(self, freq="yearly", proxy=const._SENTINEL_) -> pd.DataFrame:
|
||||
if proxy is not const._SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
res = self._cash_flow_time_series
|
||||
@@ -159,6 +159,10 @@ class Financials:
|
||||
|
||||
df.index = df.index.str.replace("^" + timescale, "", regex=True)
|
||||
|
||||
# Ensure float type, not object
|
||||
for d in df.columns:
|
||||
df[d] = df[d].astype('float')
|
||||
|
||||
# Reorder table to match order on Yahoo website
|
||||
df = df.reindex([k for k in keys if k in df.index])
|
||||
df = df[sorted(df.columns, reverse=True)]
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import pandas as pd
|
||||
|
||||
from yfinance.data import YfData
|
||||
from yfinance.const import _BASE_URL_, _SENTINEL_
|
||||
from yfinance.exceptions import YFDataException
|
||||
from yfinance import utils
|
||||
|
||||
from typing import Dict, Optional
|
||||
import warnings
|
||||
|
||||
from yfinance import utils
|
||||
from yfinance.const import _BASE_URL_, _SENTINEL_
|
||||
from yfinance.data import YfData
|
||||
from yfinance.exceptions import YFDataException
|
||||
|
||||
_QUOTE_SUMMARY_URL_ = f"{_BASE_URL_}/v10/finance/quoteSummary/"
|
||||
|
||||
@@ -26,7 +26,7 @@ class FundsData:
|
||||
self._data = data
|
||||
self._symbol = symbol
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
# quoteType
|
||||
@@ -175,7 +175,7 @@ class FundsData:
|
||||
"""
|
||||
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)
|
||||
result = self._data.get_raw_json(_QUOTE_SUMMARY_URL_+self._symbol, params=params_dict)
|
||||
return result
|
||||
|
||||
def _fetch_and_parse(self) -> None:
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
from curl_cffi import requests
|
||||
from math import isclose
|
||||
import bisect
|
||||
import datetime as _datetime
|
||||
import dateutil as _dateutil
|
||||
import logging
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from math import isclose
|
||||
import time as _time
|
||||
import bisect
|
||||
from curl_cffi import requests
|
||||
import warnings
|
||||
|
||||
from yfinance import shared, utils
|
||||
from yfinance.const import _BASE_URL_, _PRICE_COLNAMES_, _SENTINEL_
|
||||
@@ -18,7 +19,7 @@ class PriceHistory:
|
||||
self.ticker = ticker.upper()
|
||||
self.tz = tz
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=5)
|
||||
self._data._set_proxy(proxy)
|
||||
self.session = session or requests.Session(impersonate="chrome")
|
||||
|
||||
@@ -30,7 +31,7 @@ class PriceHistory:
|
||||
self._reconstruct_start_interval = None
|
||||
|
||||
@utils.log_indent_decorator
|
||||
def history(self, period="1mo", interval="1d",
|
||||
def history(self, period=None, interval="1d",
|
||||
start=None, end=None, prepost=False, actions=True,
|
||||
auto_adjust=True, back_adjust=False, repair=False, keepna=False,
|
||||
proxy=_SENTINEL_, rounding=False, timeout=10,
|
||||
@@ -38,46 +39,48 @@ class PriceHistory:
|
||||
"""
|
||||
:Parameters:
|
||||
period : str
|
||||
Valid periods: 1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max
|
||||
Either Use period parameter or use start and end
|
||||
| Valid periods: 1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max
|
||||
| Default: 1mo
|
||||
| Can combine with start/end e.g. end = start + period
|
||||
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"
|
||||
| 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: 99 years ago
|
||||
| E.g. for start="2020-01-01", first data point = "2020-01-01"
|
||||
end : str
|
||||
| Download end date string (YYYY-MM-DD) or _datetime, exclusive.
|
||||
| Default: now
|
||||
| E.g. for end="2023-01-01", last data point = "2022-12-31"
|
||||
prepost : bool
|
||||
Include Pre and Post market data in results?
|
||||
Default is False
|
||||
auto_adjust: bool
|
||||
Adjust all OHLC automatically? Default is True
|
||||
back_adjust: bool
|
||||
Back-adjusted data to mimic true historical prices
|
||||
repair: bool
|
||||
Detect currency unit 100x mixups and attempt repair.
|
||||
Default is False
|
||||
keepna: bool
|
||||
Keep NaN rows returned by Yahoo?
|
||||
Default is False
|
||||
rounding: bool
|
||||
Round values to 2 decimal places?
|
||||
Optional. Default is False = precision suggested by Yahoo!
|
||||
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)
|
||||
Default is 10 seconds.
|
||||
raise_errors: bool
|
||||
| Include Pre and Post market data in results?
|
||||
| Default: False
|
||||
auto_adjust : bool
|
||||
| Adjust all OHLC automatically?
|
||||
| Default: True
|
||||
back_adjust : bool
|
||||
| Back-adjusted data to mimic true historical prices
|
||||
repair : bool
|
||||
| Fixes price errors in Yahoo data: 100x, missing, bad dividend adjust.
|
||||
| Default: False
|
||||
| Full details at: :doc:`../advanced/price_repair`.
|
||||
keepna : bool
|
||||
| Keep NaN rows returned by Yahoo?
|
||||
| Default: False
|
||||
rounding : bool
|
||||
| Optional: Round values to 2 decimal places?
|
||||
| Default: False = use precision suggested by Yahoo!
|
||||
timeout : None or float
|
||||
| Optional: timeout fetches after N seconds
|
||||
| Default: 10 seconds
|
||||
raise_errors : bool
|
||||
If True, then raise errors as Exceptions instead of logging.
|
||||
"""
|
||||
logger = utils.get_yf_logger()
|
||||
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=5)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
interval_user = interval
|
||||
@@ -112,7 +115,7 @@ class PriceHistory:
|
||||
|
||||
start_user = start
|
||||
end_user = end
|
||||
if start or period is None or period.lower() == "max":
|
||||
if start or end or (period and period.lower() == "max"):
|
||||
# Check can get TZ. Fail => probably delisted
|
||||
tz = self.tz
|
||||
if tz is None:
|
||||
@@ -127,21 +130,49 @@ class PriceHistory:
|
||||
logger.error(err_msg)
|
||||
return utils.empty_df()
|
||||
|
||||
if end is None:
|
||||
end = int(_time.time())
|
||||
else:
|
||||
end = utils._parse_user_dt(end, tz)
|
||||
if start is None:
|
||||
if interval == "1m":
|
||||
start = end - 604800 # 7 days
|
||||
elif interval in ("5m", "15m", "30m", "90m"):
|
||||
start = end - 5184000 # 60 days
|
||||
elif interval in ("1h", '60m'):
|
||||
start = end - 63072000 # 730 days
|
||||
else:
|
||||
start = end - 3122064000 # 99 years
|
||||
else:
|
||||
start = utils._parse_user_dt(start, tz)
|
||||
if start:
|
||||
start_dt = utils._parse_user_dt(start, tz)
|
||||
start = int(start_dt.timestamp())
|
||||
if end:
|
||||
end_dt = utils._parse_user_dt(end, tz)
|
||||
end = int(end_dt.timestamp())
|
||||
|
||||
if period is None:
|
||||
if not (start or end):
|
||||
period = '1mo' # default
|
||||
elif not start:
|
||||
start_dt = end_dt - utils._interval_to_timedelta('1mo')
|
||||
start = int(start_dt.timestamp())
|
||||
elif not end:
|
||||
end_dt = pd.Timestamp.utcnow().tz_convert(tz)
|
||||
end = int(end_dt.timestamp())
|
||||
else:
|
||||
if period.lower() == "max":
|
||||
if end is None:
|
||||
end = int(_time.time())
|
||||
if start is None:
|
||||
if interval == "1m":
|
||||
start = end - 691200 # 8 days
|
||||
elif interval in ("2m", "5m", "15m", "30m", "90m"):
|
||||
start = end - 5184000 # 60 days
|
||||
elif interval in ("1h", "60m"):
|
||||
start = end - 63072000 # 730 days
|
||||
else:
|
||||
start = end - 3122064000 # 99 years
|
||||
start += 5 # allow for processing time
|
||||
elif start and end:
|
||||
raise ValueError("Setting period, start and end is nonsense. Set maximum 2 of them.")
|
||||
elif start or end:
|
||||
period_td = utils._interval_to_timedelta(period)
|
||||
if end is None:
|
||||
end_dt = start_dt + period_td
|
||||
end = int(end_dt.timestamp())
|
||||
if start is None:
|
||||
start_dt = end_dt - period_td
|
||||
start = int(start_dt.timestamp())
|
||||
period = None
|
||||
|
||||
if start or end:
|
||||
params = {"period1": start, "period2": end}
|
||||
else:
|
||||
period = period.lower()
|
||||
@@ -326,6 +357,24 @@ class PriceHistory:
|
||||
splits = utils.set_df_tz(splits, interval, tz_exchange)
|
||||
if dividends is not None:
|
||||
dividends = utils.set_df_tz(dividends, interval, tz_exchange)
|
||||
if 'currency' in dividends.columns:
|
||||
# Rare, only seen with Vietnam market
|
||||
price_currency = self._history_metadata['currency']
|
||||
if price_currency is None:
|
||||
price_currency = ''
|
||||
f_currency_mismatch = dividends['currency'] != price_currency
|
||||
if f_currency_mismatch.any():
|
||||
if not repair or price_currency == '':
|
||||
# Append currencies to values, let user decide action.
|
||||
dividends['Dividends'] = dividends['Dividends'].astype(str) + ' ' + dividends['currency']
|
||||
else:
|
||||
# Attempt repair = currency conversion
|
||||
dividends = self._dividends_convert_fx(dividends, price_currency, repair)
|
||||
if (dividends['currency'] != price_currency).any():
|
||||
# FX conversion failed
|
||||
dividends['Dividends'] = dividends['Dividends'].astype(str) + ' ' + dividends['currency']
|
||||
dividends = dividends.drop('currency', axis=1)
|
||||
|
||||
if capital_gains is not None:
|
||||
capital_gains = utils.set_df_tz(capital_gains, interval, tz_exchange)
|
||||
if start is not None:
|
||||
@@ -403,6 +452,7 @@ class PriceHistory:
|
||||
# First make currency consistent. On some exchanges, dividends often in different currency
|
||||
# to prices, e.g. £ vs pence.
|
||||
df, currency = self._standardise_currency(df, currency)
|
||||
self._history_metadata['currency'] = currency
|
||||
|
||||
df = self._fix_bad_div_adjust(df, interval, currency)
|
||||
|
||||
@@ -481,7 +531,7 @@ class PriceHistory:
|
||||
|
||||
def get_history_metadata(self, proxy=_SENTINEL_) -> dict:
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=3)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
if self._history_metadata is None or 'tradingPeriods' not in self._history_metadata:
|
||||
@@ -496,7 +546,7 @@ class PriceHistory:
|
||||
|
||||
def get_dividends(self, period="max", proxy=_SENTINEL_) -> pd.Series:
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=3)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
df = self._get_history_cache(period=period)
|
||||
@@ -507,7 +557,7 @@ class PriceHistory:
|
||||
|
||||
def get_capital_gains(self, period="max", proxy=_SENTINEL_) -> pd.Series:
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=3)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
df = self._get_history_cache(period=period)
|
||||
@@ -518,7 +568,7 @@ class PriceHistory:
|
||||
|
||||
def get_splits(self, period="max", proxy=_SENTINEL_) -> pd.Series:
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=3)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
df = self._get_history_cache(period=period)
|
||||
@@ -529,7 +579,7 @@ class PriceHistory:
|
||||
|
||||
def get_actions(self, period="max", proxy=_SENTINEL_) -> pd.Series:
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=3)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
df = self._get_history_cache(period=period)
|
||||
@@ -825,37 +875,39 @@ class PriceHistory:
|
||||
# But in case are repairing a chunk of bad 1d data, back/forward-fill the
|
||||
# good div-adjustments - not perfect, but a good backup.
|
||||
div_adjusts[f_tag] = np.nan
|
||||
div_adjusts = div_adjusts.ffill().bfill()
|
||||
for idx in np.where(f_tag)[0]:
|
||||
dt = df_new_calib.index[idx]
|
||||
n = len(div_adjusts)
|
||||
if df_new.loc[dt, "Dividends"] != 0:
|
||||
if idx < n - 1:
|
||||
# Easy, take div-adjustment from next-day
|
||||
div_adjusts.iloc[idx] = div_adjusts.iloc[idx + 1]
|
||||
if not div_adjusts.isna().all():
|
||||
# Need some real values to calibrate
|
||||
div_adjusts = div_adjusts.ffill().bfill()
|
||||
for idx in np.where(f_tag)[0]:
|
||||
dt = df_new_calib.index[idx]
|
||||
n = len(div_adjusts)
|
||||
if df_new.loc[dt, "Dividends"] != 0:
|
||||
if idx < n - 1:
|
||||
# Easy, take div-adjustment from next-day
|
||||
div_adjusts.iloc[idx] = div_adjusts.iloc[idx + 1]
|
||||
else:
|
||||
# Take previous-day div-adjustment and reverse todays adjustment
|
||||
div_adj = 1.0 - df_new_calib["Dividends"].iloc[idx] / df_new_calib['Close'].iloc[
|
||||
idx - 1]
|
||||
div_adjusts.iloc[idx] = div_adjusts.iloc[idx - 1] / div_adj
|
||||
else:
|
||||
# Take previous-day div-adjustment and reverse todays adjustment
|
||||
div_adj = 1.0 - df_new_calib["Dividends"].iloc[idx] / df_new_calib['Close'].iloc[
|
||||
idx - 1]
|
||||
div_adjusts.iloc[idx] = div_adjusts.iloc[idx - 1] / div_adj
|
||||
else:
|
||||
if idx > 0:
|
||||
# Easy, take div-adjustment from previous-day
|
||||
div_adjusts.iloc[idx] = div_adjusts.iloc[idx - 1]
|
||||
else:
|
||||
# Must take next-day div-adjustment
|
||||
div_adjusts.iloc[idx] = div_adjusts.iloc[idx + 1]
|
||||
if df_new_calib["Dividends"].iloc[idx + 1] != 0:
|
||||
div_adjusts.iloc[idx] *= 1.0 - df_new_calib["Dividends"].iloc[idx + 1] / \
|
||||
df_new_calib['Close'].iloc[idx]
|
||||
f_close_bad = df_block_calib['Close'] == tag
|
||||
div_adjusts = div_adjusts.reindex(df_block.index, fill_value=np.nan).ffill().bfill()
|
||||
df_new['Adj Close'] = df_block['Close'] * div_adjusts
|
||||
if f_close_bad.any():
|
||||
f_close_bad_new = f_close_bad.reindex(df_new.index, fill_value=False)
|
||||
div_adjusts_new = div_adjusts.reindex(df_new.index, fill_value=np.nan).ffill().bfill()
|
||||
div_adjusts_new_np = f_close_bad_new.to_numpy()
|
||||
df_new.loc[div_adjusts_new_np, 'Adj Close'] = df_new['Close'][div_adjusts_new_np] * div_adjusts_new[div_adjusts_new_np]
|
||||
if idx > 0:
|
||||
# Easy, take div-adjustment from previous-day
|
||||
div_adjusts.iloc[idx] = div_adjusts.iloc[idx - 1]
|
||||
else:
|
||||
# Must take next-day div-adjustment
|
||||
div_adjusts.iloc[idx] = div_adjusts.iloc[idx + 1]
|
||||
if df_new_calib["Dividends"].iloc[idx + 1] != 0:
|
||||
div_adjusts.iloc[idx] *= 1.0 - df_new_calib["Dividends"].iloc[idx + 1] / \
|
||||
df_new_calib['Close'].iloc[idx]
|
||||
f_close_bad = df_block_calib['Close'] == tag
|
||||
div_adjusts = div_adjusts.reindex(df_block.index, fill_value=np.nan).ffill().bfill()
|
||||
df_new['Adj Close'] = df_block['Close'] * div_adjusts
|
||||
if f_close_bad.any():
|
||||
f_close_bad_new = f_close_bad.reindex(df_new.index, fill_value=False)
|
||||
div_adjusts_new = div_adjusts.reindex(df_new.index, fill_value=np.nan).ffill().bfill()
|
||||
div_adjusts_new_np = f_close_bad_new.to_numpy()
|
||||
df_new.loc[div_adjusts_new_np, 'Adj Close'] = df_new['Close'][div_adjusts_new_np] * div_adjusts_new[div_adjusts_new_np]
|
||||
|
||||
# Check whether 'df_fine' has different split-adjustment.
|
||||
# If different, then adjust to match 'df'
|
||||
@@ -1017,6 +1069,45 @@ class PriceHistory:
|
||||
|
||||
return df, currency2
|
||||
|
||||
def _dividends_convert_fx(self, dividends, fx, repair=False):
|
||||
bad_div_currencies = [c for c in dividends['currency'].unique() if c != fx]
|
||||
major_currencies = ['USD', 'JPY', 'EUR', 'CNY', 'GBP', 'CAD']
|
||||
for c in bad_div_currencies:
|
||||
fx2_tkr = None
|
||||
if c == 'USD':
|
||||
# Simple convert from USD to target FX
|
||||
fx_tkr = f'{fx}=X'
|
||||
reverse = False
|
||||
elif fx == 'USD':
|
||||
# Use same USD FX but reversed
|
||||
fx_tkr = f'{fx}=X'
|
||||
reverse = True
|
||||
elif c in major_currencies and fx in major_currencies:
|
||||
# Simple convert
|
||||
fx_tkr = f'{c}{fx}=X'
|
||||
reverse = False
|
||||
else:
|
||||
# No guarantee that Yahoo has direct FX conversion, so
|
||||
# convert via USD
|
||||
# - step 1: -> USD
|
||||
fx_tkr = f'{c}=X'
|
||||
reverse = True
|
||||
# - step 2: USD -> FX
|
||||
fx2_tkr = f'{fx}=X'
|
||||
|
||||
fx_dat = PriceHistory(self._data, fx_tkr, self.session)
|
||||
fx_rate = fx_dat.history(period='1mo', repair=repair)['Close'].iloc[-1]
|
||||
if reverse:
|
||||
fx_rate = 1/fx_rate
|
||||
dividends.loc[dividends['currency']==c, 'Dividends'] *= fx_rate
|
||||
if fx2_tkr is not None:
|
||||
fx2_dat = PriceHistory(self._data, fx2_tkr, self.session)
|
||||
fx2_rate = fx2_dat.history(period='1mo', repair=repair)['Close'].iloc[-1]
|
||||
dividends.loc[dividends['currency']==c, 'Dividends'] *= fx2_rate
|
||||
|
||||
dividends['currency'] = fx
|
||||
return dividends
|
||||
|
||||
@utils.log_indent_decorator
|
||||
def _fix_unit_mixups(self, df, interval, tz_exchange, prepost):
|
||||
if df.empty:
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import curl_cffi
|
||||
import pandas as pd
|
||||
import requests
|
||||
import warnings
|
||||
|
||||
from yfinance import utils
|
||||
from yfinance.data import YfData
|
||||
from yfinance.const import _BASE_URL_, _SENTINEL_
|
||||
from yfinance.data import YfData
|
||||
from yfinance.exceptions import YFDataException
|
||||
|
||||
_QUOTE_SUMMARY_URL_ = f"{_BASE_URL_}/v10/finance/quoteSummary"
|
||||
@@ -15,7 +16,7 @@ class Holders:
|
||||
self._data = data
|
||||
self._symbol = symbol
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
data._set_proxy(proxy)
|
||||
|
||||
self._major = None
|
||||
@@ -67,14 +68,14 @@ class Holders:
|
||||
modules = ','.join(
|
||||
["institutionOwnership", "fundOwnership", "majorDirectHolders", "majorHoldersBreakdown", "insiderTransactions", "insiderHolders", "netSharePurchaseActivity"])
|
||||
params_dict = {"modules": modules, "corsDomain": "finance.yahoo.com", "formatted": "false"}
|
||||
result = self._data.get_raw_json(f"{_QUOTE_SUMMARY_URL_}/{self._symbol}", user_agent_headers=self._data.user_agent_headers, params=params_dict)
|
||||
result = self._data.get_raw_json(f"{_QUOTE_SUMMARY_URL_}/{self._symbol}", params=params_dict)
|
||||
return result
|
||||
|
||||
def _fetch_and_parse(self):
|
||||
try:
|
||||
result = self._fetch()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
utils.get_yf_logger().error(str(e))
|
||||
except curl_cffi.requests.exceptions.HTTPError as e:
|
||||
utils.get_yf_logger().error(str(e) + e.response.text)
|
||||
|
||||
self._major = pd.DataFrame()
|
||||
self._major_direct_holders = pd.DataFrame()
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import curl_cffi
|
||||
import datetime
|
||||
import json
|
||||
|
||||
import numpy as _np
|
||||
import pandas as pd
|
||||
import requests
|
||||
import warnings
|
||||
|
||||
from yfinance import utils
|
||||
from yfinance.data import YfData
|
||||
from yfinance.const import quote_summary_valid_modules, _BASE_URL_, _QUERY1_URL_, _SENTINEL_
|
||||
from yfinance.data import YfData
|
||||
from yfinance.exceptions import YFDataException, YFException
|
||||
|
||||
info_retired_keys_price = {"currentPrice", "dayHigh", "dayLow", "open", "previousClose", "volume", "volume24Hr"}
|
||||
@@ -29,7 +29,7 @@ class FastInfo:
|
||||
def __init__(self, tickerBaseObject, proxy=_SENTINEL_):
|
||||
self._tkr = tickerBaseObject
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._tkr._data._set_proxy(proxy)
|
||||
|
||||
self._prices_1y = None
|
||||
@@ -490,7 +490,7 @@ class Quote:
|
||||
self._data = data
|
||||
self._symbol = symbol
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
self._info = None
|
||||
@@ -587,20 +587,18 @@ class Quote:
|
||||
raise YFException("No valid modules provided, see available modules using `valid_modules`")
|
||||
params_dict = {"modules": modules, "corsDomain": "finance.yahoo.com", "formatted": "false", "symbol": self._symbol}
|
||||
try:
|
||||
result = self._data.get_raw_json(_QUOTE_SUMMARY_URL_ + f"/{self._symbol}", user_agent_headers=self._data.user_agent_headers, params=params_dict)
|
||||
except requests.exceptions.HTTPError as e:
|
||||
utils.get_yf_logger().error(str(e))
|
||||
result = self._data.get_raw_json(_QUOTE_SUMMARY_URL_ + f"/{self._symbol}", params=params_dict)
|
||||
except curl_cffi.requests.exceptions.HTTPError as e:
|
||||
utils.get_yf_logger().error(str(e) + e.response.text)
|
||||
return None
|
||||
return result
|
||||
|
||||
def _fetch_additional_info(self):
|
||||
params_dict = {"symbols": self._symbol, "formatted": "false"}
|
||||
try:
|
||||
result = self._data.get_raw_json(f"{_QUERY1_URL_}/v7/finance/quote?",
|
||||
user_agent_headers=self._data.user_agent_headers,
|
||||
params=params_dict)
|
||||
except requests.exceptions.HTTPError as e:
|
||||
utils.get_yf_logger().error(str(e))
|
||||
result = self._data.get_raw_json(f"{_QUERY1_URL_}/v7/finance/quote?", params=params_dict)
|
||||
except curl_cffi.requests.exceptions.HTTPError as e:
|
||||
utils.get_yf_logger().error(str(e) + e.response.text)
|
||||
return None
|
||||
return result
|
||||
|
||||
@@ -679,7 +677,7 @@ class Quote:
|
||||
# p = _re.compile(r'root\.App\.main = (.*);')
|
||||
# url = 'https://finance.yahoo.com/quote/{}/key-statistics?p={}'.format(self._ticker.ticker, self._ticker.ticker)
|
||||
# try:
|
||||
# r = session.get(url, headers=utils.user_agent_headers)
|
||||
# r = session.get(url)
|
||||
# data = _json.loads(p.findall(r.text)[0])
|
||||
# key_stats = data['context']['dispatcher']['stores']['QuoteTimeSeriesStore']["timeSeries"]
|
||||
# for k in keys:
|
||||
|
||||
@@ -165,7 +165,7 @@ class EquityQuery(QueryBase):
|
||||
"""
|
||||
return EQUITY_SCREENER_FIELDS
|
||||
|
||||
@dynamic_docstring({"valid_values_table": generate_list_table_from_dict_universal(EQUITY_SCREENER_EQ_MAP, concat_keys=['exchange'])})
|
||||
@dynamic_docstring({"valid_values_table": generate_list_table_from_dict_universal(EQUITY_SCREENER_EQ_MAP, concat_keys=['exchange', 'industry'])})
|
||||
@property
|
||||
def valid_values(self) -> Dict:
|
||||
"""
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
from .query import EquityQuery as EqyQy
|
||||
from .query import FundQuery as FndQy
|
||||
from .query import QueryBase, EquityQuery, FundQuery
|
||||
import curl_cffi
|
||||
from typing import Union
|
||||
import warnings
|
||||
|
||||
from yfinance.const import _QUERY1_URL_, _SENTINEL_
|
||||
from yfinance.data import YfData
|
||||
from ..utils import dynamic_docstring, generate_list_table_from_dict_universal
|
||||
|
||||
from ..utils import dynamic_docstring, generate_list_table_from_dict_universal, print_once
|
||||
|
||||
from typing import Union
|
||||
import requests
|
||||
from .query import EquityQuery as EqyQy
|
||||
from .query import FundQuery as FndQy
|
||||
from .query import QueryBase, EquityQuery, FundQuery
|
||||
|
||||
_SCREENER_URL_ = f"{_QUERY1_URL_}/v1/finance/screener"
|
||||
_PREDEFINED_URL_ = f"{_SCREENER_URL_}/predefined/saved"
|
||||
@@ -112,7 +112,7 @@ def screen(query: Union[str, EquityQuery, FundQuery],
|
||||
"""
|
||||
|
||||
if proxy is not _SENTINEL_:
|
||||
print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
_data = YfData(session=session, proxy=proxy)
|
||||
else:
|
||||
_data = YfData(session=session)
|
||||
@@ -157,7 +157,7 @@ def screen(query: Union[str, EquityQuery, FundQuery],
|
||||
# Switch to Yahoo's predefined endpoint
|
||||
|
||||
if size is not None:
|
||||
print_once("YF deprecation warning: 'size' argument is deprecated for predefined screens, set 'count' instead.")
|
||||
warnings.warn("Screen 'size' argument is deprecated for predefined screens, set 'count' instead.", DeprecationWarning, stacklevel=2)
|
||||
count = size
|
||||
size = None
|
||||
fields['count'] = fields['size']
|
||||
@@ -170,7 +170,7 @@ def screen(query: Union[str, EquityQuery, FundQuery],
|
||||
resp = _data.get(url=_PREDEFINED_URL_, params=params_dict)
|
||||
try:
|
||||
resp.raise_for_status()
|
||||
except requests.exceptions.HTTPError:
|
||||
except curl_cffi.requests.exceptions.HTTPError:
|
||||
if query not in PREDEFINED_SCREENER_QUERIES:
|
||||
print(f"yfinance.screen: '{query}' is probably not a predefined query.")
|
||||
raise
|
||||
@@ -202,7 +202,6 @@ def screen(query: Union[str, EquityQuery, FundQuery],
|
||||
# Fetch
|
||||
response = _data.post(_SCREENER_URL_,
|
||||
body=post_query,
|
||||
user_agent_headers=_data.user_agent_headers,
|
||||
params=params_dict)
|
||||
response.raise_for_status()
|
||||
return response.json()['finance']['result'][0]
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#
|
||||
|
||||
import json as _json
|
||||
import warnings
|
||||
|
||||
from . import utils
|
||||
from .const import _BASE_URL_, _SENTINEL_
|
||||
@@ -52,7 +53,7 @@ class Search:
|
||||
self._data = YfData(session=self.session)
|
||||
|
||||
if proxy is not _SENTINEL_:
|
||||
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
|
||||
self.query = query
|
||||
|
||||
@@ -22,17 +22,21 @@
|
||||
from __future__ import print_function
|
||||
|
||||
from collections import namedtuple as _namedtuple
|
||||
from .scrapers.funds import FundsData
|
||||
import warnings
|
||||
|
||||
import pandas as _pd
|
||||
|
||||
from .base import TickerBase
|
||||
from .const import _BASE_URL_, _SENTINEL_
|
||||
from .scrapers.funds import FundsData
|
||||
|
||||
|
||||
class Ticker(TickerBase):
|
||||
def __init__(self, ticker, session=None, proxy=_SENTINEL_):
|
||||
super(Ticker, self).__init__(ticker, session=session, proxy=proxy)
|
||||
if proxy is not _SENTINEL_:
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
super(Ticker, self).__init__(ticker, session=session)
|
||||
self._expirations = {}
|
||||
self._underlying = {}
|
||||
|
||||
|
||||
@@ -21,9 +21,10 @@
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import warnings
|
||||
|
||||
from . import Ticker, multi
|
||||
from .live import WebSocket
|
||||
from .utils import print_once
|
||||
from .data import YfData
|
||||
from .const import _SENTINEL_
|
||||
|
||||
@@ -56,8 +57,9 @@ class Tickers:
|
||||
timeout=10, **kwargs):
|
||||
|
||||
if proxy is not _SENTINEL_:
|
||||
print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
proxy = _SENTINEL_
|
||||
|
||||
return self.download(
|
||||
period, interval,
|
||||
@@ -75,8 +77,9 @@ class Tickers:
|
||||
timeout=10, **kwargs):
|
||||
|
||||
if proxy is not _SENTINEL_:
|
||||
print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
warnings.warn("Set proxy via new config function: yf.set_config(proxy=proxy)", DeprecationWarning, stacklevel=2)
|
||||
self._data._set_proxy(proxy)
|
||||
proxy = _SENTINEL_
|
||||
|
||||
data = multi.download(self.symbols,
|
||||
start=start, end=end,
|
||||
|
||||
@@ -27,7 +27,7 @@ import re
|
||||
import re as _re
|
||||
import sys as _sys
|
||||
import threading
|
||||
from functools import lru_cache, wraps
|
||||
from functools import wraps
|
||||
from inspect import getmembers
|
||||
from types import FunctionType
|
||||
from typing import List, Optional
|
||||
@@ -40,9 +40,6 @@ from pytz import UnknownTimeZoneError
|
||||
|
||||
from yfinance import const
|
||||
|
||||
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'}
|
||||
|
||||
# From https://stackoverflow.com/a/59128615
|
||||
def attributes(obj):
|
||||
disallowed_names = {
|
||||
@@ -53,13 +50,6 @@ def attributes(obj):
|
||||
if name[0] != '_' and name not in disallowed_names and hasattr(obj, name)}
|
||||
|
||||
|
||||
@lru_cache(maxsize=20)
|
||||
def print_once(msg):
|
||||
# 'warnings' module suppression of repeat messages does not work.
|
||||
# This function replicates correct behaviour
|
||||
print(msg)
|
||||
|
||||
|
||||
# Logging
|
||||
# Note: most of this logic is adding indentation with function depth,
|
||||
# so that DEBUG log is readable.
|
||||
@@ -184,18 +174,14 @@ def is_isin(string):
|
||||
return bool(_re.match("^([A-Z]{2})([A-Z0-9]{9})([0-9])$", string))
|
||||
|
||||
|
||||
def get_all_by_isin(isin, proxy=const._SENTINEL_, session=None):
|
||||
def get_all_by_isin(isin):
|
||||
if not (is_isin(isin)):
|
||||
raise ValueError("Invalid ISIN number")
|
||||
|
||||
if proxy is not const._SENTINEL_:
|
||||
print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
|
||||
proxy = None
|
||||
|
||||
# Deferred this to prevent circular imports
|
||||
from .search import Search
|
||||
|
||||
search = Search(query=isin, max_results=1, session=session, proxy=proxy)
|
||||
search = Search(query=isin, max_results=1)
|
||||
|
||||
# Extract the first quote and news
|
||||
ticker = search.quotes[0] if search.quotes else {}
|
||||
@@ -213,18 +199,18 @@ def get_all_by_isin(isin, proxy=const._SENTINEL_, session=None):
|
||||
}
|
||||
|
||||
|
||||
def get_ticker_by_isin(isin, proxy=const._SENTINEL_, session=None):
|
||||
data = get_all_by_isin(isin, proxy, session)
|
||||
def get_ticker_by_isin(isin):
|
||||
data = get_all_by_isin(isin)
|
||||
return data.get('ticker', {}).get('symbol', '')
|
||||
|
||||
|
||||
def get_info_by_isin(isin, proxy=const._SENTINEL_, session=None):
|
||||
data = get_all_by_isin(isin, proxy, session)
|
||||
def get_info_by_isin(isin):
|
||||
data = get_all_by_isin(isin)
|
||||
return data.get('ticker', {})
|
||||
|
||||
|
||||
def get_news_by_isin(isin, proxy=const._SENTINEL_, session=None):
|
||||
data = get_all_by_isin(isin, proxy, session)
|
||||
def get_news_by_isin(isin):
|
||||
data = get_all_by_isin(isin)
|
||||
return data.get('news', {})
|
||||
|
||||
|
||||
@@ -415,18 +401,21 @@ def snake_case_2_camelCase(s):
|
||||
|
||||
def _parse_user_dt(dt, exchange_tz):
|
||||
if isinstance(dt, int):
|
||||
# Should already be epoch, test with conversion:
|
||||
_datetime.datetime.fromtimestamp(dt)
|
||||
dt = _pd.Timestamp(dt, unit="s", tz=exchange_tz)
|
||||
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())
|
||||
if isinstance(dt, _datetime.datetime):
|
||||
if dt.tzinfo is None:
|
||||
# Assume user is referring to exchange's timezone
|
||||
dt = _pd.Timestamp(dt).tz_localize(exchange_tz)
|
||||
else:
|
||||
dt = _pd.Timestamp(dt).tz_convert(exchange_tz)
|
||||
else: # if we reached here, then it hasn't been any known type
|
||||
raise ValueError(f"Unable to parse input dt {dt} of type {type(dt)}")
|
||||
return dt
|
||||
|
||||
|
||||
@@ -533,7 +522,10 @@ def parse_actions(data):
|
||||
dividends.set_index("date", inplace=True)
|
||||
dividends.index = _pd.to_datetime(dividends.index, unit="s")
|
||||
dividends.sort_index(inplace=True)
|
||||
dividends.columns = ["Dividends"]
|
||||
if 'currency' in dividends.columns and (dividends['currency'] == '').all():
|
||||
# Currency column useless, drop it.
|
||||
dividends = dividends.drop('currency', axis=1)
|
||||
dividends = dividends.rename(columns={'amount': 'Dividends'})
|
||||
|
||||
if "capitalGains" in data["events"] and len(data["events"]['capitalGains']) > 0:
|
||||
capital_gains = _pd.DataFrame(
|
||||
@@ -758,6 +750,13 @@ def safe_merge_dfs(df_main, df_sub, interval):
|
||||
if df_sub.empty:
|
||||
df_main['Dividends'] = 0.0
|
||||
return df_main
|
||||
|
||||
# df_sub changed so recalc indices:
|
||||
df_main['_date'] = df_main.index.date
|
||||
df_sub['_date'] = df_sub.index.date
|
||||
indices = _np.searchsorted(_np.append(df_main['_date'], [df_main['_date'].iloc[-1]+td]), df_sub['_date'], side='left')
|
||||
df_main = df_main.drop('_date', axis=1)
|
||||
df_sub = df_sub.drop('_date', axis=1)
|
||||
else:
|
||||
empty_row_data = {**{c:[_np.nan] for c in const._PRICE_COLNAMES_}, 'Volume':[0]}
|
||||
if interval == '1d':
|
||||
|
||||
@@ -1 +1 @@
|
||||
version = "0.2.58"
|
||||
version = "0.2.66"
|
||||
|
||||
Reference in New Issue
Block a user