Fundamental Analysis with Real Filings¶

Open In Colab

P/E, market cap, debt/equity and revenue growth pulled live via yfinance — Reliance vs Apple, side by side.

Part 26 of 35 in the ServLoci algo/options trading notebook series — full index in notebooks/README.md.

Setup — no broker account needed¶

!pip install -q yfinance

import yfinance as yf
import pandas as pd
import numpy as np

# No broker account, no API key, no ServLoci setup needed for this chapter —
# yfinance reads public end-of-day data from Yahoo Finance. Indian tickers take
# an ".NS" suffix (NSE) or ".BO" (BSE); index tickers are prefixed with "^"
# (^NSEI = Nifty 50, ^BSESN = Sensex, ^GSPC = S&P 500).
NSE_TICKER = "RELIANCE.NS"
US_TICKER = "AAPL"
INDEX_TICKER = "^NSEI"

print("yfinance", yf.__version__)

Price tells you what; filings tell you why¶

Most algo-trading courses skip fundamentals entirely — they jump straight to candles and indicators, as if a ticker were just a stream of numbers with no company behind it. Most stock-market-literacy courses do the opposite: they teach fundamentals as static theory (read the balance sheet, check the P/E) with no code, no real filing, nothing you can run. Neither habit reflects how a serious investor actually works: price is what the market is willing to pay right now; fundamentals are the evidence for whether that price is reasonable.

yfinance exposes the same fundamentals data a terminal would, for free, with no broker account: Ticker.info (a snapshot dict), Ticker.financials / .quarterly_financials (income statement), .balance_sheet, and .cashflow. This chapter pulls real numbers for NSE_TICKER and US_TICKER side by side — not because Reliance and Apple are comparable businesses, but because comparing a familiar US mega-cap against an Indian large-cap makes it obvious which fundamentals are universal (P/E, margins) and which need local context (currency, sector norms, promoter holding conventions that don't exist for US filings).

nse = yf.Ticker(NSE_TICKER)
us = yf.Ticker(US_TICKER)

def snapshot(ticker):
    info = ticker.info
    return {
        "marketCap": info.get("marketCap"),
        "trailingPE": info.get("trailingPE"),
        "priceToBook": info.get("priceToBook"),
        "dividendYield": info.get("dividendYield"),
        "profitMargins": info.get("profitMargins"),
        "returnOnEquity": info.get("returnOnEquity"),
        "debtToEquity": info.get("debtToEquity"),
        "sector": info.get("sector"),
        "freeCashflow": info.get("freeCashflow"),
    }

compare = pd.DataFrame({NSE_TICKER: snapshot(nse), US_TICKER: snapshot(us)})
print(compare)

Reading the numbers¶

  • P/E (price-to-earnings): what the market is paying per rupee/dollar of last year's profit. High P/E means the market is pricing in growth that hasn't happened yet — that's a bet, not a fact. A "cheap" P/E can just mean the market correctly expects earnings to fall.
  • P/B (price-to-book): price against accounting net worth. More useful for asset-heavy businesses (banks, manufacturers) than asset-light ones (software), where most of the value is in things a balance sheet doesn't capture.
  • Dividend yield: cash return, independent of price appreciation. A yield that looks unusually high is often the market pricing in a dividend cut, not a gift.
  • ROE (return on equity): how efficiently the company turns shareholder capital into profit. High ROE funded by high debt is a different, riskier story than high ROE funded by retained earnings — which is exactly why the next cell doesn't stop at .info.
  • Debt/equity: leverage. .info's cached value can be stale by a quarter or more; the balance sheet below is the primary source.
nse_financials = nse.financials  # annual income statement, most recent period first
if "Total Revenue" in nse_financials.index and nse_financials.loc["Total Revenue"].notna().sum() >= 2:
    revenue = nse_financials.loc["Total Revenue"].dropna()
    yoy_growth = revenue.iloc[0] / revenue.iloc[1] - 1
    print(f"{NSE_TICKER} YoY revenue growth (latest two annual filings): {yoy_growth:.1%}")
else:
    print(f"{NSE_TICKER}: not enough annual revenue history returned to compute YoY growth.")

nse_balance = nse.balance_sheet
if "Total Debt" in nse_balance.index and "Common Stock Equity" in nse_balance.index:
    debt = nse_balance.loc["Total Debt"].iloc[0]
    equity = nse_balance.loc["Common Stock Equity"].iloc[0]
    print(f"{NSE_TICKER} balance-sheet debt/equity (most recent filing): {debt / equity:.2f}")
else:
    print(f"{NSE_TICKER}: balance sheet did not return the expected line items — field names vary by ticker and exchange.")

Where this breaks¶

  • .info fields are cached snapshots and vary by ticker — some Indian tickers return fewer fields than US ones, and a missing key means "not reported here," not zero.
  • Fundamentals lag price by a full quarter at best; a great balance sheet six months ago says nothing about what changed last week.
  • A metric in isolation is close to meaningless. A P/E of 40 is expensive for a slow-growing utility and cheap for a company compounding earnings at 40% a year — you have to compare against the sector and the company's own growth rate (the P/E-to-growth, or PEG, idea), not a fixed threshold.
  • None of this predicts price movement on any particular day. Fundamentals answer "is this a business worth owning," not "should I buy in the next five minutes" — that second question is what notebook 27 turns to next, and it comes with its own honesty problem.

« Previous: Returns, Volatility & the Numbers Courses Skip
Next: Technical Indicators, Tested Honestly »

Try the concepts above interactively: Options Strategy Builder · Docs · Get your static IP