Skip to content

Commit

Permalink
Direct TTM Financial Data Retrieval Feature
Browse files Browse the repository at this point in the history
- Fetches the TTM values for cash flow and income statement from Yahoo.
- Added tests test_ttm_income_statement and test_ttm_cash_flow.
- When trailing 12 months income statement is fetched Yahoo returns
  income statement for last 5 quarters out of which only the latest is
  fully populated. Rest have NaN values and can be ignored.
- get_financials_time_series when called with timescale trailing removes
  from data frame the extra columns.
  • Loading branch information
JanMkl committed Feb 26, 2025
1 parent f2a8d01 commit e727807
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 4 deletions.
78 changes: 78 additions & 0 deletions tests/test_ticker.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,12 @@
("recommendations", Union[pd.DataFrame, dict]),
("recommendations_summary", Union[pd.DataFrame, dict]),
("upgrades_downgrades", Union[pd.DataFrame, dict]),
("ttm_cashflow", pd.DataFrame),
("quarterly_cashflow", pd.DataFrame),
("cashflow", pd.DataFrame),
("quarterly_balance_sheet", pd.DataFrame),
("balance_sheet", pd.DataFrame),
("ttm_income_stmt", pd.DataFrame),
("quarterly_income_stmt", pd.DataFrame),
("income_stmt", pd.DataFrame),
("analyst_price_targets", dict),
Expand Down Expand Up @@ -554,6 +556,34 @@ def test_quarterly_income_statement(self):
data = self.ticker.get_income_stmt(as_dict=True)
self.assertIsInstance(data, dict, "data has wrong type")

def test_ttm_income_statement(self):
expected_keys = ["Total Revenue", "Pretax Income", "Normalized EBITDA"]

# Test contents of table
data = self.ticker.get_income_stmt(pretty=True, freq='trailing')
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
self.assertFalse(data.empty, "data is empty")
for k in expected_keys:
self.assertIn(k, data.index, "Did not find expected row in index")
# Trailing 12 months there must be exactly one column
self.assertEqual(len(data.columns), 1, "Only one column should be returned on TTM income statement")

# Test property defaults
data2 = self.ticker.ttm_income_stmt
self.assertTrue(data.equals(data2), "property not defaulting to 'pretty=True'")

# Test pretty=False
expected_keys = [k.replace(' ', '') for k in expected_keys]
data = self.ticker.get_income_stmt(pretty=False, freq='trailing')
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
self.assertFalse(data.empty, "data is empty")
for k in expected_keys:
self.assertIn(k, data.index, "Did not find expected row in index")

# Test to_dict
data = self.ticker.get_income_stmt(as_dict=True, freq='trailing')
self.assertIsInstance(data, dict, "data has wrong type")

def test_balance_sheet(self):
expected_keys = ["Total Assets", "Net PPE"]
expected_periods_days = 365
Expand Down Expand Up @@ -670,6 +700,34 @@ def test_quarterly_cash_flow(self):
data = self.ticker.get_cashflow(as_dict=True)
self.assertIsInstance(data, dict, "data has wrong type")

def test_ttm_cash_flow(self):
expected_keys = ["Operating Cash Flow", "Net PPE Purchase And Sale"]

# Test contents of table
data = self.ticker.get_cashflow(pretty=True, freq='trailing')
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
self.assertFalse(data.empty, "data is empty")
for k in expected_keys:
self.assertIn(k, data.index, "Did not find expected row in index")
# Trailing 12 months there must be exactly one column
self.assertEqual(len(data.columns), 1, "Only one column should be returned on TTM cash flow")

# Test property defaults
data2 = self.ticker.ttm_cashflow
self.assertTrue(data.equals(data2), "property not defaulting to 'pretty=True'")

# Test pretty=False
expected_keys = [k.replace(' ', '') for k in expected_keys]
data = self.ticker.get_cashflow(pretty=False, freq='trailing')
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
self.assertFalse(data.empty, "data is empty")
for k in expected_keys:
self.assertIn(k, data.index, "Did not find expected row in index")

# Test to_dict
data = self.ticker.get_cashflow(as_dict=True, freq='trailing')
self.assertIsInstance(data, dict, "data has wrong type")

def test_income_alt_names(self):
i1 = self.ticker.income_stmt
i2 = self.ticker.incomestmt
Expand All @@ -695,6 +753,18 @@ def test_income_alt_names(self):
i3 = self.ticker.get_financials(freq="quarterly")
self.assertTrue(i1.equals(i3))

i1 = self.ticker.ttm_income_stmt
i2 = self.ticker.ttm_incomestmt
self.assertTrue(i1.equals(i2))
i3 = self.ticker.ttm_financials
self.assertTrue(i1.equals(i3))

i1 = self.ticker.get_income_stmt(freq="trailing")
i2 = self.ticker.get_incomestmt(freq="trailing")
self.assertTrue(i1.equals(i2))
i3 = self.ticker.get_financials(freq="trailing")
self.assertTrue(i1.equals(i3))

def test_balance_sheet_alt_names(self):
i1 = self.ticker.balance_sheet
i2 = self.ticker.balancesheet
Expand Down Expand Up @@ -729,6 +799,14 @@ def test_cash_flow_alt_names(self):
i2 = self.ticker.get_cashflow(freq="quarterly")
self.assertTrue(i1.equals(i2))

i1 = self.ticker.ttm_cash_flow
i2 = self.ticker.ttm_cashflow
self.assertTrue(i1.equals(i2))

i1 = self.ticker.get_cash_flow(freq="trailing")
i2 = self.ticker.get_cashflow(freq="trailing")
self.assertTrue(i1.equals(i2))

def test_bad_freq_value_raises_exception(self):
self.assertRaises(ValueError, lambda: self.ticker.get_cashflow(freq="badarg"))

Expand Down
4 changes: 2 additions & 2 deletions yfinance/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ def get_earnings(self, proxy=None, as_dict=False, freq="yearly"):
Return table as Python dict
Default is False
freq: str
"yearly" or "quarterly"
"yearly" or "quarterly" or "trailing"
Default is "yearly"
proxy: str
Optional. Proxy server URL scheme
Expand All @@ -345,7 +345,7 @@ def get_income_stmt(self, proxy=None, as_dict=False, pretty=False, freq="yearly"
Format row names nicely for readability
Default is False
freq: str
"yearly" or "quarterly"
"yearly" or "quarterly" or "trailing"
Default is "yearly"
proxy: str
Optional. Proxy server URL scheme
Expand Down
11 changes: 9 additions & 2 deletions yfinance/scrapers/fundamentals.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,15 @@ def _fetch_time_series(self, name, timescale, proxy=None):
# despite 'QuoteSummaryStore' containing valid data.

allowed_names = ["income", "balance-sheet", "cash-flow"]
allowed_timescales = ["yearly", "quarterly"]
allowed_timescales = ["yearly", "quarterly", "trailing"]

if name not in allowed_names:
raise ValueError(f"Illegal argument: name must be one of: {allowed_names}")
if timescale not in allowed_timescales:
raise ValueError(f"Illegal argument: timescale must be one of: {allowed_timescales}")
if timescale == "trailing" and name not in ('income', 'cash-flow'):
raise ValueError("Illegal argument: frequency 'trailing'" +
" only available for cash-flow or income data.")

try:
statement = self._create_financials_table(name, timescale, proxy)
Expand All @@ -102,7 +105,7 @@ def _create_financials_table(self, name, timescale, proxy):
pass

def get_financials_time_series(self, timescale, keys: list, proxy=None) -> pd.DataFrame:
timescale_translation = {"yearly": "annual", "quarterly": "quarterly"}
timescale_translation = {"yearly": "annual", "quarterly": "quarterly", "trailing": "trailing"}
timescale = timescale_translation[timescale]

# Step 2: construct url:
Expand Down Expand Up @@ -145,4 +148,8 @@ def get_financials_time_series(self, timescale, keys: list, proxy=None) -> pd.Da
df = df.reindex([k for k in keys if k in df.index])
df = df[sorted(df.columns, reverse=True)]

# Trailing 12 months return only the first column.
if (timescale == "trailing"):
df = df.iloc[:, [0]]

return df
20 changes: 20 additions & 0 deletions yfinance/ticker.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,10 @@ def income_stmt(self) -> _pd.DataFrame:
def quarterly_income_stmt(self) -> _pd.DataFrame:
return self.get_income_stmt(pretty=True, freq='quarterly')

@property
def ttm_income_stmt(self) -> _pd.DataFrame:
return self.get_income_stmt(pretty=True, freq='trailing')

@property
def incomestmt(self) -> _pd.DataFrame:
return self.income_stmt
Expand All @@ -209,6 +213,10 @@ def incomestmt(self) -> _pd.DataFrame:
def quarterly_incomestmt(self) -> _pd.DataFrame:
return self.quarterly_income_stmt

@property
def ttm_incomestmt(self) -> _pd.DataFrame:
return self.ttm_income_stmt

@property
def financials(self) -> _pd.DataFrame:
return self.income_stmt
Expand All @@ -217,6 +225,10 @@ def financials(self) -> _pd.DataFrame:
def quarterly_financials(self) -> _pd.DataFrame:
return self.quarterly_income_stmt

@property
def ttm_financials(self) -> _pd.DataFrame:
return self.ttm_income_stmt

@property
def balance_sheet(self) -> _pd.DataFrame:
return self.get_balance_sheet(pretty=True)
Expand All @@ -241,6 +253,10 @@ def cash_flow(self) -> _pd.DataFrame:
def quarterly_cash_flow(self) -> _pd.DataFrame:
return self.get_cash_flow(pretty=True, freq='quarterly')

@property
def ttm_cash_flow(self) -> _pd.DataFrame:
return self.get_cash_flow(pretty=True, freq='trailing')

@property
def cashflow(self) -> _pd.DataFrame:
return self.cash_flow
Expand All @@ -249,6 +265,10 @@ def cashflow(self) -> _pd.DataFrame:
def quarterly_cashflow(self) -> _pd.DataFrame:
return self.quarterly_cash_flow

@property
def ttm_cashflow(self) -> _pd.DataFrame:
return self.ttm_cash_flow

@property
def analyst_price_targets(self) -> dict:
return self.get_analyst_price_targets()
Expand Down

0 comments on commit e727807

Please sign in to comment.