Compare commits

...

19 Commits

Author SHA1 Message Date
ValueRaider
c8d34863c1 better idea: check if table exists after init 2024-09-04 22:20:36 +01:00
ValueRaider
94eac4308b Cache: possible fix for 'no such table' 2024-09-04 22:13:42 +01:00
ValueRaider
8cadad2cbb Merge pull request #2048 from ranaroussi/main
sync main -> dev
2024-09-04 21:44:28 +01:00
ValueRaider
3fe87cb132 Merge pull request #2039 from joncokisler/main
Fix typo in README.md #2038
2024-08-26 19:27:55 +01:00
Jon Cokisler
72684b1784 fix typo in README 2024-08-26 13:51:45 -04:00
ValueRaider
ed43f6fc09 Version 0.2.43 2024-08-24 21:37:58 +01:00
ValueRaider
f87a9affb3 Merge pull request #2036 from ranaroussi/fix/price-repair-currency-bug
Fix price-repair-currency, logic was inverted
2024-08-24 21:36:36 +01:00
ValueRaider
e329f267b2 Fix price-repair-currency, logic was inverted 2024-08-24 19:04:15 +01:00
ValueRaider
bd1a597a0c Version 0.2.42 2024-08-22 16:59:11 +01:00
ValueRaider
0193cec8bf Merge pull request #2034 from ranaroussi/dev
sync dev -> main
2024-08-22 16:53:14 +01:00
ValueRaider
a2b5d6bea9 Fix 2x old tests 2024-08-22 16:29:07 +01:00
ValueRaider
ce9becddbb Merge branch 'main' into dev 2024-08-22 16:12:15 +01:00
ValueRaider
7e12f2029f Merge pull request #2031 from ranaroussi/feature/price-repair-div-adjust
New: repair bad dividends and div-adjusts. Plus other repair fixes.
2024-08-22 16:03:13 +01:00
ValueRaider
7c66bc374c Merge pull request #2032 from ranaroussi/revert/PR-2027-30m-resampling
Revert PR #2027, breaks with prepost=True
2024-08-22 11:08:07 +01:00
ValueRaider
a0dc25229b Update python-publish.yml to v4 for node20 2024-08-21 22:35:27 +01:00
ValueRaider
1baecc9d5b Revert PR #2027, breaks with prepost=True 2024-08-21 21:11:52 +01:00
ValueRaider
8b8db167f5 Update bug_report.yaml 2024-08-19 10:10:57 +01:00
ValueRaider
459d5f69c2 Merge pull request #2016 from mreiche/bugfix/mixed-tz-conversion
Fix datetime conversion with mixed timezones when ignore_tz is False
2024-08-12 20:29:54 +01:00
Mike Reiche
f13ff4bb4c Fix datetime conversion with mixed timezones when ignore_tz is False 2024-08-12 07:39:55 +02:00
11 changed files with 135 additions and 64 deletions

View File

@@ -55,7 +55,7 @@ body:
id: debug-log
attributes:
label: "Debug log"
description: "Run code with debug logging enabled and post the full output. IMPORTANT INSTRUCTIONS: https://github.com/ranaroussi/yfinance/tree/main#logging"
description: "Run code with debug logging enabled - `yf.enable_debug_mode()` - and post the full output. Context: https://github.com/ranaroussi/yfinance/tree/main#logging"
validations:
required: true

View File

@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:

View File

@@ -1,6 +1,24 @@
Change Log
===========
0.2.43
------
Fix price-repair bug introduced in 0.2.42 #2036
0.2.42
------
Features:
- fetch SEC filings #2009
- fetch analysis #2023 @Fidasek009
- price repair extended to dividends & adjust #2031
Fixes:
- fix error on empty options chain #1995 @stevenbischoff
- use dict.get() to safely access key in Holders #2013 @ericpien
- fix datetime conversion with mixed timezones when ignore_tz is False #2016 @mreiche
- handle faulty response object when getting news. #2021 @ericpien
Maintenance:
- prices: improve exceptions and logging #2000
0.2.41
------
Improvements:

View File

@@ -121,7 +121,7 @@ msft.insider_transactions
msft.insider_purchases
msft.insider_roster_holders
msfs.sustainability
msft.sustainability
# show recommendations
msft.recommendations

View File

@@ -1,5 +1,5 @@
{% set name = "yfinance" %}
{% set version = "0.2.41" %}
{% set version = "0.2.43" %}
package:
name: "{{ name|lower }}"

View File

@@ -31,17 +31,23 @@ class TestPriceHistory(unittest.TestCase):
f = df.index.time == _dt.time(0)
self.assertTrue(f.all())
def test_download(self):
def test_download_multi_large_interval(self):
tkrs = ["BHP.AX", "IMP.JO", "BP.L", "PNL.L", "INTC"]
intervals = ["1d", "1wk", "1mo"]
for interval in intervals:
df = yf.download(tkrs, period="5y", interval=interval)
with self.subTest(interval):
df = yf.download(tkrs, period="5y", interval=interval)
f = df.index.time == _dt.time(0)
self.assertTrue(f.all())
f = df.index.time == _dt.time(0)
self.assertTrue(f.all())
df_tkrs = df.columns.levels[1]
self.assertEqual(sorted(tkrs), sorted(df_tkrs))
df_tkrs = df.columns.levels[1]
self.assertEqual(sorted(tkrs), sorted(df_tkrs))
def test_download_multi_small_interval(self):
use_tkrs = ["AAPL", "0Q3.DE", "ATVI"]
df = yf.download(use_tkrs, period="1d", interval="5m")
self.assertEqual(df.index.tz, _dt.timezone.utc)
def test_download_with_invalid_ticker(self):
#Checks if using an invalid symbol gives the same output as not using an invalid symbol in combination with a valid symbol (AAPL)
@@ -172,9 +178,8 @@ class TestPriceHistory(unittest.TestCase):
if df_daily_divs.shape[0] == 0:
continue
last_div_date = df_daily_divs.index[-1]
start_d = last_div_date.date()
end_d = last_div_date.date() + _dt.timedelta(days=1)
start_d = df_daily_divs.index[0].date()
end_d = df_daily_divs.index[-1].date() + _dt.timedelta(days=1)
df_intraday = yf.Ticker(tkr, session=self.session).history(start=start_d, end=end_d, interval="15m", actions=True)
self.assertTrue((df_intraday["Dividends"] != 0.0).any())
@@ -200,9 +205,8 @@ class TestPriceHistory(unittest.TestCase):
if df_daily_divs.shape[0] == 0:
continue
last_div_date = df_daily_divs.index[-1]
start_d = last_div_date.date()
end_d = last_div_date.date() + _dt.timedelta(days=1)
start_d = df_daily_divs.index[0].date()
end_d = df_daily_divs.index[-1].date() + _dt.timedelta(days=1)
df_intraday = yf.Ticker(tkr, session=self.session).history(start=start_d, end=end_d, interval="15m", actions=True)
self.assertTrue((df_intraday["Dividends"] != 0.0).any())

View File

@@ -8,9 +8,10 @@ Specific test class:
python -m unittest tests.utils.TestTicker
"""
from datetime import datetime
from unittest import TestSuite
# import pandas as pd
import pandas as pd
# import numpy as np
from .context import yfinance as yf
@@ -81,10 +82,34 @@ class TestCacheNoPermission(unittest.TestCase):
cache.lookup(tkr)
class TestPandas(unittest.TestCase):
date_strings = ["2024-08-07 09:05:00+02:00", "2024-08-07 09:05:00-04:00"]
@unittest.expectedFailure
def test_mixed_timezones_to_datetime_fails(self):
series = pd.Series(self.date_strings)
series = series.map(pd.Timestamp)
converted = pd.to_datetime(series)
self.assertIsNotNone(converted[0].tz)
def test_mixed_timezones_to_datetime(self):
series = pd.Series(self.date_strings)
series = series.map(pd.Timestamp)
converted = pd.to_datetime(series, utc=True)
self.assertIsNotNone(converted[0].tz)
i = 0
for dt in converted:
dt: datetime
ts: pd.Timestamp = series[i]
self.assertEqual(dt.isoformat(), ts.tz_convert(tz="UTC").isoformat())
i += 1
def suite():
ts: TestSuite = unittest.TestSuite()
ts.addTest(TestCache('Test cache'))
ts.addTest(TestCacheNoPermission('Test cache no permission'))
ts.addTest(TestPandas("Test pandas"))
return ts

View File

@@ -153,6 +153,18 @@ class _TzCache:
db.create_tables([_KV])
else:
raise
# # Verify that the database file actually exists.
# # Maybe peewee silently failed to create file and
# # fellback to in-memory database:
# if not _os.path.exists(db.database):
# self.initialised = 0 # failure
# return
# Verify that the table was actually created
if not db.table_exists('_kv'):
self.initialised = 0 # failure
return
self.initialised = 1 # success
def lookup(self, key):

View File

@@ -211,7 +211,7 @@ def download(tickers, start=None, end=None, actions=False, threads=True, ignore_
_realign_dfs()
data = _pd.concat(shared._DFS.values(), axis=1, sort=True,
keys=shared._DFS.keys(), names=['Ticker', 'Price'])
data.index = _pd.to_datetime(data.index)
data.index = _pd.to_datetime(data.index, utc=True)
# switch names back to isins if applicable
data.rename(columns=shared._ISINS, inplace=True)

View File

@@ -258,9 +258,7 @@ class PriceHistory:
# 2) fix weired bug with Yahoo! - returning 60m for 30m bars
if interval.lower() == "30m":
logger.debug(f'{self.ticker}: resampling 30m OHLC from 15m')
exchangeStartTime = pd.Timestamp(self._history_metadata["tradingPeriods"][0][0]["start"], unit='s')
offset = str(exchangeStartTime.minute % 30)+"min"
quotes2 = quotes.resample('30min', offset=offset)
quotes2 = quotes.resample('30min')
quotes = pd.DataFrame(index=quotes2.last().index, data={
'Open': quotes2['Open'].first(),
'High': quotes2['High'].max(),
@@ -370,48 +368,7 @@ class PriceHistory:
# Must fix bad 'Adj Close' & dividends before 100x/split errors.
# First make currency consistent. On some exchanges, dividends often in different currency
# to prices, e.g. £ vs pence.
if currency in ["GBp", "ZAc", "ILA"]:
if currency == 'GBp':
# UK £/pence
currency = 'GBP'
m = 0.01
elif currency == 'ZAc':
# South Africa Rand/cents
currency = 'ZAR'
m = 0.01
elif currency == 'ILA':
# Israel Shekels/Agora
currency = 'ILS'
m = 0.01
prices_in_subunits = True # usually is true
if df.index[-1] > (pd.Timestamp.utcnow() - _datetime.timedelta(days=30)):
try:
ratio = self._history_metadata['regularMarketPrice'] / self._history_metadata['chartPreviousClose']
if abs((ratio*m)-1) < 0.1:
# within 10% of 100x
prices_in_subunits = True
except Exception:
pass
if prices_in_subunits:
for c in _PRICE_COLNAMES_:
df[c] *= m
self._history_metadata["currency"] = currency
f_div = df['Dividends']!=0.0
if f_div.any():
# But sometimes the dividend was in pence.
# Heuristic is: if dividend yield is ridiculous high vs converted prices, then
# assume dividend was also in pence and convert to GBP.
# Threshold for "ridiculous" based on largest yield I've seen anywhere - 63.4%
# If this simple heuritsic generates a false positive, then _fix_bad_div_adjust()
# will detect and repair.
divs = df[['Close','Dividends']].copy()
divs['Close'] = divs['Close'].ffill().shift(1, fill_value=divs['Close'].iloc[0])
divs = divs[f_div]
div_pcts = (divs['Dividends'] / divs['Close']).to_numpy()
if len(div_pcts) > 0 and np.average(div_pcts) > 1:
df['Dividends'] *= m
df, currency = self._standardise_currency(df, currency)
df = self._fix_bad_div_adjust(df, interval, currency)
@@ -935,6 +892,61 @@ class PriceHistory:
return df_v2
def _standardise_currency(self, df, currency):
if currency not in ["GBp", "ZAc", "ILA"]:
return df, currency
currency2 = currency
if currency == 'GBp':
# UK £/pence
currency2 = 'GBP'
m = 0.01
elif currency == 'ZAc':
# South Africa Rand/cents
currency2 = 'ZAR'
m = 0.01
elif currency == 'ILA':
# Israel Shekels/Agora
currency2 = 'ILS'
m = 0.01
# Use latest row with actual volume, because volume=0 rows can be 0.01x the other rows.
# _fix_unit_switch() will ensure all rows are on same scale.
f_volume = df['Volume']>0
if not f_volume.any():
return df, currency
last_row = df.iloc[np.where(f_volume)[0][-1]]
prices_in_subunits = True # usually is true
if last_row.name > (pd.Timestamp.utcnow() - _datetime.timedelta(days=30)):
try:
ratio = self._history_metadata['regularMarketPrice'] / last_row['Close']
if abs((ratio*m)-1) < 0.1:
# within 10% of 100x
prices_in_subunits = False
except Exception:
# Should never happen but just-in-case
pass
if prices_in_subunits:
for c in _PRICE_COLNAMES_:
df[c] *= m
self._history_metadata["currency"] = currency
f_div = df['Dividends']!=0.0
if f_div.any():
# But sometimes the dividend was in pence.
# Heuristic is: if dividend yield is ridiculous high vs converted prices, then
# assume dividend was also in pence and convert to GBP.
# Threshold for "ridiculous" based on largest yield I've seen anywhere - 63.4%
# If this simple heuristic generates a false positive, then _fix_bad_div_adjust()
# will detect and repair.
divs = df[['Close','Dividends']].copy()
divs['Close'] = divs['Close'].ffill().shift(1, fill_value=divs['Close'].iloc[0])
divs = divs[f_div]
div_pcts = (divs['Dividends'] / divs['Close']).to_numpy()
if len(div_pcts) > 0 and np.average(div_pcts) > 1:
df['Dividends'] *= m
return df, currency2
@utils.log_indent_decorator
def _fix_unit_mixups(self, df, interval, tz_exchange, prepost):
if df.empty:

View File

@@ -1 +1 @@
version = "0.2.41"
version = "0.2.43"