
Financial Analysis Dcf
- 75 installs
- 5 repo stars
- Updated July 24, 2026
- pionex-official/pionex-skills
Helps with ai & agent building tasks.
About
financial-analysis-dcf is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- financial-analysis-dcf
- AI & Agent Building
- AI-coding skill
Financial Analysis Dcf by the numbers
- 75 all-time installs (skills.sh)
- +3 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #5,486 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/pionex-official/pionex-skills --skill financial-analysis-dcfAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 75 |
|---|---|
| repo stars | ★ 5 |
| Last updated | July 24, 2026 |
| Repository | pionex-official/pionex-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
DCF Valuation
Estimate intrinsic value per share using a discounted cash flow model. Data from SEC EDGAR (financials), FRED (risk-free rate), and Yahoo Finance (price, beta).
Setup
No dependencies required. All scripts use Python standard library only.
Workflow
Step 1 — Run the DCF script
bash run.sh <SYMBOL>
# With custom assumptions:
bash run.sh <SYMBOL> --terminal-growth 3.0 --fcf-growth 15 --projection-years 7The script: 1. Fetches 5 years of 10-K data from SEC EDGAR 2. Computes unlevered FCF = OCF − CapEx + InterestExpense × (1−T) 3. Derives historical FCF growth rate (or uses override) 4. Gets risk-free rate from FRED (10Y Treasury) 5. Gets beta and current price from Yahoo Finance 6. Calculates WACC = Ke × E/V + Kd × (1−T) × D/V 7. Projects FCFs, computes terminal value (Gordon Growth Model) 8. Generates 5×5 sensitivity table (WACC vs terminal growth)
Step 2 — Section 1: Summary
First output must be this summary box:
[TICKER] — DCF VALUATION
Intrinsic value: $XXX.XX | Current price: $XXX.XX | ▲/▼ XX.X%
WACC: X.X% | Terminal growth: X.X% | FCF base: $X.XBStep 3 — Section 2: Sensitivity Table
Show the full 5×5 sensitivity table from the JSON output:
Terminal Growth \\ WACC | 8.2% | 9.2% | 10.2%
------------------------|---------|---------|--------
1.5% | $198.4 | $176.2 | $158.1
2.5% | $221.3 | $194.5 | $172.6
3.5% | $251.7 | $218.0 | $191.4Display N/A for null values (WACC ≤ terminal growth).
Step 4 — Section 3: Interpretation
Note key assumptions and their impact:
- Which inputs drive the most variance (usually WACC and growth rate)
- Whether the beta seems reasonable for the company
- How the historical FCF growth rate compares to analyst expectations
- Any red flags (negative FCF years, volatile cash flows, high debt)
Disclaimer: DCF output is highly sensitive to WACC and growth assumptions. It should be used as one input among many, not as a definitive price target. Always present the sensitivity table alongside the point estimate.
---
Model Assumptions
- ERP: 5.5% (Damodaran market average)
- Tax rate: derived from SEC filings (IncomeTaxExpense / PreTaxIncome); fallback 21%
- Cost of debt: derived from SEC filings (InterestExpense / LongTermDebt); fallback 4%
- FCF: Unlevered = OCF − CapEx + InterestExpense × (1−T) from most recent 10-K
- FCF growth: per-share UFCF CAGR from historical 10-K data (accounts for buybacks); fallback 3% if insufficient data
- Beta: OLS regression on daily returns vs SPY (~5 years); Blume adjustment applied (adjusted = 0.67 × raw + 0.33)
- Shares outstanding: from SEC EDGAR; fallback to market cap / price
- Mid-year convention: FCFs and terminal value both discounted at mid-year
Formatting Rules
- Intrinsic value and current price: "$XXX.XX"
- Upside/downside: "▲ +XX.X%" or "▼ −XX.X%"
- WACC and growth rates: one decimal, e.g. "9.2%"
- FCF: B or M, e.g. "$12.3B"
When NOT to Use
- Negative or volatile FCF: companies with negative free cash flow cannot be valued via DCF — the model requires a positive base FCF to project forward
- Banks and financial institutions: revenue and cash flow structures differ fundamentally (interest income vs. operating revenue); use price-to-book or dividend discount models instead
- Insurance companies: similar to banks — earnings driven by underwriting and investment income, not operating cash flow
- Pre-revenue or early-stage companies: no meaningful FCF history to extrapolate
Limitations
- US stocks only: SEC EDGAR data for US-listed equities
- Backward-looking: uses historical financials — future may differ significantly
- No analyst estimates: growth rates extrapolated from historical data unless user provides one
#!/usr/bin/env bash
# DCF Valuation — no external dependencies
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
exec python3 "$SCRIPT_DIR/scripts/get_dcf.py" "$@"
#!/usr/bin/env python3
"""
Calculate stock Beta using OLS regression on daily returns vs SPY.
Usage:
python get_beta.py AAPL
python get_beta.py MSFT --window 252
Uses Yahoo Finance historical prices (no API key needed).
Applies Blume adjustment: adjusted_beta = 0.67 * raw_beta + 0.33
Output: JSON with raw_beta, adjusted_beta, data_points, and period.
"""
import argparse
import json
import sys
import urllib.request
import urllib.error
import time
from datetime import datetime, timedelta, timezone
BENCHMARK = "SPY"
DEFAULT_WINDOW = 1260 # ~5 years of trading days
def _fetch_daily_closes(symbol: str, days: int) -> dict[str, float]:
"""Fetch daily close prices from Yahoo Finance. Returns {date_str: close}."""
end = int(time.time())
# Add buffer days for weekends/holidays
start = end - (days + 500) * 86400
url = (
f"https://query1.finance.yahoo.com/v8/finance/chart/{symbol}"
f"?period1={start}&period2={end}&interval=1d"
)
req = urllib.request.Request(url)
req.add_header("User-Agent", "Mozilla/5.0")
try:
with urllib.request.urlopen(req, timeout=15) as resp:
data = json.loads(resp.read())
except (urllib.error.URLError, urllib.error.HTTPError):
return {}
result_data = data.get("chart", {}).get("result", [])
if not result_data:
return {}
timestamps = result_data[0].get("timestamp", [])
closes = result_data[0].get("indicators", {}).get("quote", [{}])[0].get("close", [])
closes_map = {}
for ts, close in zip(timestamps, closes):
if close is not None:
day = datetime.fromtimestamp(ts, tz=timezone.utc).strftime("%Y-%m-%d")
closes_map[day] = close
return closes_map
def calculate_beta(symbol: str, window: int = DEFAULT_WINDOW) -> dict:
"""Calculate beta of symbol vs SPY."""
symbol = symbol.upper()
stock_closes = _fetch_daily_closes(symbol, window)
if not stock_closes:
return {"symbol": symbol, "error": f"Failed to fetch price data for {symbol}"}
spy_closes = _fetch_daily_closes(BENCHMARK, window)
if not spy_closes:
return {"symbol": symbol, "error": "Failed to fetch SPY price data"}
# Intersect dates
common_dates = sorted(set(stock_closes.keys()) & set(spy_closes.keys()))
if len(common_dates) < 11:
return {"symbol": symbol, "raw_beta": 1.0, "adjusted_beta": 1.0,
"data_points": len(common_dates), "note": "Insufficient data, defaulting to 1.0"}
# Compute aligned daily returns
stock_returns = []
market_returns = []
for i in range(1, len(common_dates)):
prev, cur = common_dates[i - 1], common_dates[i]
sp, sc = stock_closes[prev], stock_closes[cur]
mp, mc = spy_closes[prev], spy_closes[cur]
if sp <= 0 or mp <= 0:
continue
stock_returns.append((sc - sp) / sp)
market_returns.append((mc - mp) / mp)
m = len(stock_returns)
if m < 10:
return {"symbol": symbol, "raw_beta": 1.0, "adjusted_beta": 1.0,
"data_points": m, "note": "Insufficient return data, defaulting to 1.0"}
# OLS: beta = Cov(stock, market) / Var(market)
mean_s = sum(stock_returns) / m
mean_m = sum(market_returns) / m
cov = 0.0
var_m = 0.0
for i in range(m):
ds = stock_returns[i] - mean_s
dm = market_returns[i] - mean_m
cov += ds * dm
var_m += dm * dm
if var_m == 0:
raw_beta = 1.0
else:
raw_beta = cov / var_m
# Blume adjustment: shrinks toward 1.0 for better forward estimate
adjusted_beta = 0.67 * raw_beta + 0.33
return {
"symbol": symbol,
"benchmark": BENCHMARK,
"raw_beta": round(raw_beta, 4),
"adjusted_beta": round(adjusted_beta, 4),
"data_points": m,
"period": f"{common_dates[0]} to {common_dates[-1]}",
"window_days": window,
}
def main():
parser = argparse.ArgumentParser(description="Calculate stock Beta vs SPY")
parser.add_argument("symbol", help="Stock ticker (e.g., AAPL)")
parser.add_argument("--window", type=int, default=DEFAULT_WINDOW,
help=f"Trading days for regression (default: {DEFAULT_WINDOW})")
args = parser.parse_args()
result = calculate_beta(args.symbol, args.window)
print(json.dumps(result, indent=2))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
DCF (Discounted Cash Flow) valuation using SEC EDGAR + Yahoo Finance.
Usage:
python get_dcf.py AAPL
python get_dcf.py MSFT --terminal-growth 3.0 --projection-years 7
Output: JSON with intrinsic value, assumptions, and sensitivity table.
No pip dependencies. Uses SEC EDGAR for financials, Yahoo Finance for price/beta.
"""
import argparse
import json
import math
import os
import sys
import urllib.request
import urllib.error
sys.path.insert(0, os.path.dirname(__file__))
from get_fundamentals import fetch_fundamentals
from get_quote import fetch_quote
# Constants
ERP = 5.5 # Equity Risk Premium (Damodaran)
DEFAULT_TAX_RATE = 0.21
DEFAULT_COST_OF_DEBT = 4.0
DEFAULT_TERMINAL_GROWTH = 2.5
DEFAULT_PROJECTION_YEARS = 5
DEFAULT_RISK_FREE_RATE = 4.0
def _get_risk_free_rate() -> float:
"""Fetch 10-year US Treasury yield from FRED CSV endpoint (no API key needed)."""
import csv
import io
url = "https://fred.stlouisfed.org/graph/fredgraph.csv?id=DGS10"
try:
req = urllib.request.Request(url)
req.add_header("User-Agent", "finchat-skills contact@finchat.ai")
with urllib.request.urlopen(req, timeout=15) as resp:
text = resp.read().decode("utf-8")
reader = csv.reader(io.StringIO(text))
next(reader) # skip header
latest = None
for row in reader:
if len(row) >= 2 and row[1] != ".":
try:
latest = float(row[1])
except ValueError:
continue
if latest is not None:
return latest
except Exception:
pass
return DEFAULT_RISK_FREE_RATE
def _calc_dcf(base_fcf, growth_rate, wacc, terminal_growth, years, debt, cash, shares):
"""Core DCF calculation. Returns (ev, equity_value, intrinsic_per_share)."""
if wacc <= terminal_growth:
return -1, -1, -1
pv_fcf = 0.0
fcf = base_fcf
for i in range(1, years + 1):
fcf *= (1 + growth_rate)
pv_fcf += fcf / math.pow(1 + wacc, i - 0.5) # mid-year convention
# Terminal value (Gordon Growth Model)
terminal_fcf = fcf * (1 + terminal_growth)
terminal_value = terminal_fcf / (wacc - terminal_growth)
pv_terminal = terminal_value / math.pow(1 + wacc, years + 0.5) # mid-year convention
ev = pv_fcf + pv_terminal
net_debt = debt - cash
equity = max(0, ev - net_debt)
intrinsic = equity / shares if shares > 0 else 0
return ev, equity, intrinsic
def _historical_fcf_growth(periods: list[dict]) -> float:
"""Calculate mean annualized per-share UFCF growth from SEC EDGAR periods."""
entries = []
for p in periods:
ocf = p.get("operating_cash_flow") or 0
capex = p.get("capital_expenditure") or 0
interest = p.get("interest_expense") or 0
pretax = p.get("pretax_income") or 0
tax_exp = p.get("income_tax_expense") or 0
shares = p.get("shares_outstanding") or 0
tr = DEFAULT_TAX_RATE
if pretax > 0 and tax_exp > 0:
tr = max(0.05, min(0.40, tax_exp / pretax))
capex = abs(capex)
fcf = ocf - capex + interest * (1 - tr)
if fcf > 0 and shares > 0:
entries.append({"year": p.get("fiscal_year", 0), "fcf_per_share": fcf / shares})
if len(entries) < 2:
return 3.0
total = 0.0
count = 0
for i in range(len(entries) - 1):
curr, prev = entries[i], entries[i + 1]
span = curr["year"] - prev["year"]
if span <= 0 or prev["fcf_per_share"] <= 0:
continue
annualized = (math.pow(curr["fcf_per_share"] / prev["fcf_per_share"], 1.0 / span) - 1) * 100
total += annualized
count += 1
return round(total / count, 2) if count > 0 else 3.0
def run_dcf(symbol: str, fcf_growth_override: float | None = None, terminal_growth: float = DEFAULT_TERMINAL_GROWTH,
projection_years: int = DEFAULT_PROJECTION_YEARS) -> dict:
symbol = symbol.upper()
# 1. Fetch 5 years of annual financials
data = fetch_fundamentals(symbol, form="10-K", periods=5)
if data.get("error"):
return {"symbol": symbol, "error": data["error"]}
periods = data.get("periods", [])
if not periods:
return {"symbol": symbol, "error": "No annual data available"}
entity_name = data.get("entity_name", symbol)
latest = periods[0]
# 2. Tax rate and cost of debt
ltd = latest.get("long_term_debt") or 0
interest = latest.get("interest_expense") or 0
pretax = latest.get("pretax_income") or 0
tax_exp = latest.get("income_tax_expense") or 0
kd = DEFAULT_COST_OF_DEBT
if ltd > 0 and interest > 0:
kd = max(1, min(15, (interest / ltd) * 100))
tax_rate = DEFAULT_TAX_RATE
if pretax > 0 and tax_exp > 0:
tax_rate = max(0.05, min(0.40, tax_exp / pretax))
# 3. Base FCF (unlevered)
ocf = latest.get("operating_cash_flow") or 0
capex = abs(latest.get("capital_expenditure") or 0)
interest_addback = interest * (1 - tax_rate)
base_fcf = ocf - capex + interest_addback
if base_fcf <= 0:
return {"symbol": symbol, "error": f"Negative FCF (OCF={ocf}, CapEx={capex}). DCF requires positive FCF."}
# 4. FCF growth rate
fcf_growth = fcf_growth_override if fcf_growth_override is not None else _historical_fcf_growth(periods)
fcf_growth = max(-20, min(50, fcf_growth))
# 5. Risk-free rate
rf_rate = _get_risk_free_rate()
# 6. Beta (OLS regression vs SPY, Blume-adjusted) and current price
from get_beta import calculate_beta
beta_result = calculate_beta(symbol)
beta = beta_result.get("adjusted_beta") or 1.0
quote_data = fetch_quote([symbol])
quote = quote_data["quotes"][0] if quote_data.get("quotes") else {}
current_price = quote.get("current_price") or 0
market_cap = quote.get("market_cap") or 0
# 7. Cost of equity (CAPM)
ke = rf_rate + beta * ERP
# 8. Shares — prefer Yahoo Finance (more current), fallback to SEC EDGAR
shares = quote.get("shares_outstanding") or latest.get("shares_outstanding") or 0
if shares <= 0 and current_price > 0 and market_cap > 0:
shares = market_cap / current_price
# 9. WACC
if market_cap <= 0 and current_price > 0 and shares > 0:
market_cap = current_price * shares
debt = ltd
total_capital = market_cap + debt
we = market_cap / total_capital if total_capital > 0 else 1.0
wd = debt / total_capital if total_capital > 0 else 0.0
wacc = round(ke * we + kd * (1 - tax_rate) * wd, 2)
# 10. DCF calculation
ev, equity, intrinsic = _calc_dcf(
base_fcf, fcf_growth / 100, wacc / 100, terminal_growth / 100,
projection_years, debt, latest.get("cash_and_equivalents") or 0, shares
)
upside = round((intrinsic / current_price - 1) * 100, 2) if current_price > 0 and intrinsic > 0 else 0
# 11. Sensitivity table
wacc_range = [wacc - 1, wacc - 0.5, wacc, wacc + 0.5, wacc + 1]
tg_range = [terminal_growth - 1, terminal_growth - 0.5, terminal_growth, terminal_growth + 0.5, terminal_growth + 1]
sensitivity = []
for w in wacc_range:
row = {"wacc_pct": round(w, 1), "intrinsic_by_terminal_growth": {}}
for tg in tg_range:
_, _, iv = _calc_dcf(
base_fcf, fcf_growth / 100, w / 100, tg / 100,
projection_years, debt, latest.get("cash_and_equivalents") or 0, shares
)
key = f"{tg:.1f}%"
row["intrinsic_by_terminal_growth"][key] = round(iv, 2) if iv >= 0 else None
sensitivity.append(row)
return {
"symbol": symbol,
"entity_name": entity_name,
"current_price": round(current_price, 2),
"intrinsic_value_per_share": round(intrinsic, 2),
"upside_downside_pct": upside,
"enterprise_value": round(ev),
"equity_value": round(equity),
"shares_outstanding": round(shares),
"assumptions": {
"risk_free_rate_pct": round(rf_rate, 2),
"beta": round(beta, 2),
"beta_data_points": beta_result.get("data_points"),
"beta_period": beta_result.get("period"),
"equity_risk_premium_pct": ERP,
"cost_of_equity_pct": round(ke, 2),
"cost_of_debt_pct": round(kd, 2),
"tax_rate_pct": round(tax_rate * 100, 2),
"wacc_pct": wacc,
"fcf_growth_rate_pct": round(fcf_growth, 2),
"terminal_growth_rate_pct": terminal_growth,
"projection_years": projection_years,
"base_fcf": round(base_fcf),
},
"sensitivity_wacc_vs_terminal_growth": sensitivity,
"sources": {
"financials": "SEC EDGAR (XBRL)",
"risk_free_rate": "FRED (DGS10)",
"beta": "OLS regression vs SPY (Blume-adjusted)",
"price": "Yahoo Finance",
},
"note": "Intrinsic value is highly sensitive to WACC and growth assumptions. Use the sensitivity table for a range of outcomes. null in the table means WACC <= terminal growth (undefined).",
}
def main():
parser = argparse.ArgumentParser(description="DCF valuation (SEC EDGAR + Yahoo Finance)")
parser.add_argument("symbol", help="Stock ticker (e.g., AAPL)")
parser.add_argument("--fcf-growth", type=float, default=None, help="Override FCF growth rate (%%)")
parser.add_argument("--terminal-growth", type=float, default=DEFAULT_TERMINAL_GROWTH, help="Terminal growth rate (%%)")
parser.add_argument("--projection-years", type=int, default=DEFAULT_PROJECTION_YEARS, help="Projection years")
args = parser.parse_args()
result = run_dcf(args.symbol, args.fcf_growth, args.terminal_growth, args.projection_years)
print(json.dumps(result, indent=2, default=str))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Fetch quarterly and annual financial statements from SEC EDGAR XBRL API.
Usage:
python get_fundamentals.py <SYMBOL> [--form 10-K|10-Q] [--periods N]
Output: JSON with an array of financial periods.
Data source: SEC EDGAR (https://data.sec.gov)
No API key required. No external dependencies.
Rate limited to ~8 req/s per SEC guidelines.
Important: 10-Q data is YTD cumulative. Derive standalone quarters by subtraction:
Q1 standalone = Q1 YTD
Q2 standalone = H1 YTD - Q1 YTD
Q3 standalone = 9M YTD - H1 YTD
Q4 standalone = Full-year (10-K) - 9M YTD
"""
import argparse
import json
import sys
import time
import urllib.request
from datetime import datetime
BASE_URL = "https://data.sec.gov"
TICKER_URL = "https://www.sec.gov/files/company_tickers.json"
USER_AGENT = "finchat-skills contact@finchat.ai"
MIN_REQUEST_INTERVAL = 0.12 # ~8 req/s
_last_request_at = 0.0
_ticker_cik_map = None
def _throttle():
global _last_request_at
elapsed = time.time() - _last_request_at
if elapsed < MIN_REQUEST_INTERVAL:
time.sleep(MIN_REQUEST_INTERVAL - elapsed)
_last_request_at = time.time()
def _get_json(url: str) -> dict:
_throttle()
req = urllib.request.Request(url)
req.add_header("User-Agent", USER_AGENT)
req.add_header("Accept", "application/json")
try:
with urllib.request.urlopen(req, timeout=30) as resp:
return json.loads(resp.read())
except (urllib.request.URLError, urllib.error.HTTPError) as e:
raise RuntimeError(f"SEC EDGAR request failed for {url}: {e}")
def _ensure_ticker_map() -> dict[str, int]:
global _ticker_cik_map
if _ticker_cik_map is not None:
return _ticker_cik_map
raw = _get_json(TICKER_URL)
_ticker_cik_map = {}
for entry in raw.values():
_ticker_cik_map[entry["ticker"].upper()] = entry["cik_str"]
return _ticker_cik_map
def _lookup_cik(ticker: str) -> int:
m = _ensure_ticker_map()
cik = m.get(ticker.upper())
if cik is None:
raise ValueError(f"Ticker {ticker!r} not found in SEC EDGAR")
return int(cik)
# ── XBRL parsing ──
def _first_concept(usgaap: dict, *names: str):
"""Return the concept with the most recent data among the given names."""
best = None
best_end = ""
for name in names:
concept = usgaap.get(name)
if not concept:
continue
units = concept.get("units", {})
for unit_facts in units.values():
for f in unit_facts:
end = f.get("end", "")
if end > best_end:
best_end = end
best = concept
break
return best
def _facts_for_form(concept, form: str) -> dict[str, float]:
"""Extract period_end -> value map for a filing form type."""
m = {}
if not concept:
return m
units = concept.get("units", {})
for unit_facts in units.values():
for f in unit_facts:
if f.get("form") == form:
end = f.get("end", "")
if end not in m:
m[end] = f.get("val", 0)
break
return m
def _anchor_periods(concept, form: str, n: int) -> list[dict]:
"""Return the most recent N filing facts for a form type."""
if not concept:
return []
facts = []
units = concept.get("units", {})
for unit_facts in units.values():
for f in unit_facts:
if f.get("form") == form:
facts.append(f)
break
facts.sort(key=lambda f: f.get("end", ""), reverse=True)
seen = set()
deduped = []
for f in facts:
end = f.get("end", "")
if end not in seen:
seen.add(end)
deduped.append(f)
return deduped[:n] if n > 0 else deduped
def _fiscal_quarter_from_period(start: str, end: str, form: str) -> tuple[int, bool]:
"""Derive fiscal quarter and YTD flag from filing dates.
10-K -> quarter=4, isYTD=False
10-Q ~3mo -> Q1, ~6mo -> Q2, ~9mo -> Q3, all isYTD=True
"""
if form == "10-K":
return 4, False
if not start or not end:
return 0, True
try:
t0 = datetime.strptime(start, "%Y-%m-%d")
t1 = datetime.strptime(end, "%Y-%m-%d")
except ValueError:
return 0, True
days = (t1 - t0).days
if days < 105:
return 1, True
elif days < 196:
return 2, True
else:
return 3, True
def _period_label(start: str, end: str, fy: int, fq: int, form: str) -> str:
"""Build human-readable label, e.g. 'FY2025 Q1 (Oct-Dec 2024)'."""
if not start or not end:
return f"FY{fy} Annual" if form == "10-K" else f"FY{fy} Q{fq}"
try:
t0 = datetime.strptime(start, "%Y-%m-%d")
t1 = datetime.strptime(end, "%Y-%m-%d")
except ValueError:
return f"FY{fy} Annual" if form == "10-K" else f"FY{fy} Q{fq}"
s = t0.strftime("%b %Y")
e = t1.strftime("%b %Y")
if form == "10-K":
return f"FY{fy} Annual ({s}\u2013{e})"
return f"FY{fy} Q{fq} ({s}\u2013{e})"
def _eps_for_period(eps: float, is_ytd: bool, quarter: int):
"""Return EPS only when directly usable as standalone figure.
Q2/Q3 YTD EPS is NOT additive — zeroed to prevent misuse."""
if not is_ytd:
return eps # 10-K full-year
if quarter == 1:
return eps # Q1 YTD == Q1 standalone
return None # Q2/Q3: must compute from standalone net_income / shares
def _derived_gross_profit(gp, revenue, cost_of_revenue):
if gp is not None:
return gp
if revenue and cost_of_revenue:
return revenue - cost_of_revenue
return None
def _normalize_shares_scale(shares, net_income, eps):
"""Correct for companies reporting shares in thousands or millions."""
if not shares or shares <= 0 or not eps or not net_income:
return shares if shares else None
implied = abs(net_income) / abs(eps)
ratio = implied / shares
if 500 <= ratio < 5000:
return shares * 1000
elif 500_000 <= ratio < 5_000_000:
return shares * 1_000_000
return shares
# ── Main fetch ──
def fetch_fundamentals(symbol: str, form: str = "10-K", periods: int = 4) -> dict:
symbol = symbol.upper()
if periods <= 0:
periods = 4
elif periods > 8:
periods = 8
cik = _lookup_cik(symbol)
url = f"{BASE_URL}/api/xbrl/companyfacts/CIK{cik:010d}.json"
raw = _get_json(url)
entity_name = raw.get("entityName", symbol)
usgaap = raw.get("facts", {}).get("us-gaap")
if not usgaap:
return {"symbol": symbol, "entity_name": entity_name, "error": "No US-GAAP data available", "periods": []}
rev_concept = _first_concept(usgaap,
"Revenues",
"RevenueFromContractWithCustomerExcludingAssessedTax",
"RevenueFromContractWithCustomerIncludingAssessedTax",
"SalesRevenueNet",
"SalesRevenueGoodsNet",
)
anchor_concept = rev_concept or usgaap.get("NetIncomeLoss")
if not anchor_concept:
return {"symbol": symbol, "entity_name": entity_name, "error": "No financial data found", "periods": []}
anchors = _anchor_periods(anchor_concept, form, periods)
if not anchors:
return {"symbol": symbol, "entity_name": entity_name, "error": f"No {form} filings found", "periods": []}
# Build period->value lookup maps
rev_map = _facts_for_form(rev_concept, form)
ni_map = _facts_for_form(_first_concept(usgaap, "NetIncomeLoss", "NetIncomeLossAttributableToParent", "ProfitLoss"), form)
gp_map = _facts_for_form(_first_concept(usgaap, "GrossProfit", "GrossProfitLoss"), form)
cor_map = _facts_for_form(_first_concept(usgaap, "CostOfRevenue", "CostOfGoodsAndServicesSold", "CostOfGoodsSold"), form)
oi_map = _facts_for_form(_first_concept(usgaap, "OperatingIncomeLoss"), form)
eps_map = _facts_for_form(_first_concept(usgaap, "EarningsPerShareDiluted"), form)
ocf_map = _facts_for_form(_first_concept(usgaap, "NetCashProvidedByUsedInOperatingActivities"), form)
capex_map = _facts_for_form(_first_concept(usgaap,
"PaymentsToAcquirePropertyPlantAndEquipment",
"PaymentsForCapitalImprovements",
"PaymentsToAcquireOtherProductiveAssets",
), form)
assets_map = _facts_for_form(usgaap.get("Assets"), form)
liab_map = _facts_for_form(usgaap.get("Liabilities"), form)
equity_map = _facts_for_form(_first_concept(usgaap,
"StockholdersEquity",
"StockholdersEquityIncludingPortionAttributableToNoncontrollingInterest",
), form)
ltd_map = _facts_for_form(_first_concept(usgaap, "LongTermDebt", "LongTermDebtNoncurrent"), form)
interest_map = _facts_for_form(_first_concept(usgaap, "InterestExpense", "InterestAndDebtExpense"), form)
tax_map = _facts_for_form(_first_concept(usgaap, "IncomeTaxExpenseBenefit"), form)
pretax_map = _facts_for_form(_first_concept(usgaap,
"IncomeLossFromContinuingOperationsBeforeIncomeTaxesExtraordinaryItemsNoncontrollingInterest",
"IncomeLossFromContinuingOperationsBeforeIncomeTaxesMinorityInterestAndIncomeLossFromEquityMethodInvestments",
), form)
cash_map = _facts_for_form(_first_concept(usgaap,
"CashAndCashEquivalentsAtCarryingValue",
"CashCashEquivalentsAndShortTermInvestments",
"CashCashEquivalentsRestrictedCashAndRestrictedCashEquivalents",
), form)
shares_map = _facts_for_form(_first_concept(usgaap,
"CommonStockSharesOutstanding",
"WeightedAverageNumberOfDilutedSharesOutstanding",
), form)
results = []
for af in anchors:
end = af.get("end", "")
start = af.get("start", "")
fy = af.get("fy", 0)
q, is_ytd = _fiscal_quarter_from_period(start, end, form)
raw_eps = eps_map.get(end, 0)
eps = _eps_for_period(raw_eps, is_ytd, q)
net_income = ni_map.get(end, 0)
shares = _normalize_shares_scale(shares_map.get(end, 0), net_income, raw_eps)
revenue = rev_map.get(end, 0)
gross_profit = _derived_gross_profit(gp_map.get(end), revenue, cor_map.get(end, 0))
period = {
"period": end,
"form": form,
"fiscal_year": fy,
"fiscal_quarter": q,
"is_ytd": is_ytd,
"period_label": _period_label(start, end, fy, q, form),
"revenue": revenue if revenue else None,
"gross_profit": gross_profit,
"operating_income": oi_map.get(end),
"net_income": net_income if net_income else None,
"eps_diluted": eps,
"pretax_income": pretax_map.get(end),
"interest_expense": interest_map.get(end),
"income_tax_expense": tax_map.get(end),
"total_assets": assets_map.get(end),
"total_liabilities": liab_map.get(end),
"shareholders_equity": equity_map.get(end),
"cash_and_equivalents": cash_map.get(end),
"long_term_debt": ltd_map.get(end),
"shares_outstanding": shares,
"operating_cash_flow": ocf_map.get(end),
"capital_expenditure": capex_map.get(end),
}
results.append(period)
return {
"symbol": symbol,
"entity_name": entity_name,
"form": form,
"periods": results,
}
def main():
parser = argparse.ArgumentParser(description="Fetch financial fundamentals from SEC EDGAR")
parser.add_argument("symbol", help="Stock ticker symbol (e.g., AAPL)")
parser.add_argument("--form", choices=["10-K", "10-Q"], default="10-K", help="Filing type (default: 10-K)")
parser.add_argument("--periods", type=int, default=4, help="Number of periods to fetch (default: 4)")
args = parser.parse_args()
result = fetch_fundamentals(args.symbol.upper(), args.form, args.periods)
print(json.dumps(result, indent=2, default=str))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Fetch current stock quote from Yahoo Finance.
Usage:
python get_quote.py AAPL [MSFT GOOGL ...]
Output: JSON with current price, market cap, shares outstanding, PE, etc.
No pip dependencies — uses Yahoo Finance endpoints via urllib with cookie+crumb auth.
"""
import argparse
import json
import sys
import urllib.request
import urllib.error
import http.cookiejar
YF_CHART_URL = "https://query1.finance.yahoo.com/v8/finance/chart/{symbol}?range=1d&interval=1d"
YF_CRUMB_URL = "https://query2.finance.yahoo.com/v1/test/getcrumb"
YF_SUMMARY_URL = "https://query2.finance.yahoo.com/v10/finance/quoteSummary/{symbol}?modules=price,defaultKeyStatistics,summaryDetail&crumb={crumb}"
_opener = None
_crumb = None
def _init_session():
"""Initialize cookie jar and fetch crumb (one-time per process)."""
global _opener, _crumb
if _opener is not None:
return
cj = http.cookiejar.CookieJar()
_opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))
_opener.addheaders = [("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)")]
# Step 1: hit a Yahoo page to get cookies
try:
_opener.open("https://fc.yahoo.com/", timeout=10)
except urllib.error.HTTPError:
pass # 404 is expected, but cookies are set
except Exception:
pass
# Step 2: fetch crumb using those cookies
try:
resp = _opener.open(YF_CRUMB_URL, timeout=10)
_crumb = resp.read().decode("utf-8").strip()
except Exception:
_crumb = None
def _yf_get(url: str, use_session: bool = False) -> dict | None:
"""Fetch JSON from Yahoo Finance."""
try:
if use_session and _opener:
resp = _opener.open(url, timeout=10)
else:
req = urllib.request.Request(url)
req.add_header("User-Agent", "Mozilla/5.0")
resp = urllib.request.urlopen(req, timeout=10)
return json.loads(resp.read())
except (urllib.error.URLError, urllib.error.HTTPError, json.JSONDecodeError):
return None
def _raw_val(obj):
"""Extract raw value from Yahoo Finance formatted object like {'raw': 1.23, 'fmt': '1.23'}."""
if isinstance(obj, dict):
return obj.get("raw")
return obj
def fetch_quote(symbols: list[str]) -> dict:
_init_session()
quotes = []
for sym in symbols:
sym = sym.upper()
try:
# Chart endpoint for price (reliable, no auth needed)
data = _yf_get(YF_CHART_URL.format(symbol=sym))
if not data:
quotes.append({"symbol": sym, "error": "Failed to fetch quote"})
continue
result_list = data.get("chart", {}).get("result")
if not result_list:
quotes.append({"symbol": sym, "error": "No chart data"})
continue
meta = result_list[0].get("meta", {})
quote = {
"symbol": sym,
"name": meta.get("longName") or meta.get("shortName") or sym,
"current_price": meta.get("regularMarketPrice"),
"previous_close": meta.get("previousClose") or meta.get("chartPreviousClose"),
"market_cap": None,
"shares_outstanding": None,
"enterprise_value": None,
"volume": meta.get("regularMarketVolume"),
"fifty_two_week_high": meta.get("fiftyTwoWeekHigh"),
"fifty_two_week_low": meta.get("fiftyTwoWeekLow"),
"pe_trailing": None,
"pe_forward": None,
"eps_trailing": None,
"beta": None,
"dividend_yield": None,
"currency": meta.get("currency"),
"exchange": meta.get("exchangeName"),
}
# Try quoteSummary with crumb for richer data
if _crumb:
summary = _yf_get(
YF_SUMMARY_URL.format(symbol=sym, crumb=urllib.request.quote(_crumb)),
use_session=True,
)
if summary:
modules = summary.get("quoteSummary", {}).get("result")
if modules:
mod = modules[0]
price = mod.get("price", {})
stats = mod.get("defaultKeyStatistics", {})
detail = mod.get("summaryDetail", {})
quote["name"] = _raw_val(price.get("longName")) or price.get("shortName") or quote["name"]
quote["market_cap"] = _raw_val(price.get("marketCap"))
quote["shares_outstanding"] = _raw_val(stats.get("sharesOutstanding")) or _raw_val(price.get("sharesOutstanding"))
quote["enterprise_value"] = _raw_val(stats.get("enterpriseValue"))
quote["pe_trailing"] = _raw_val(detail.get("trailingPE"))
quote["pe_forward"] = _raw_val(stats.get("forwardPE"))
quote["eps_trailing"] = _raw_val(stats.get("trailingEps"))
quote["beta"] = _raw_val(stats.get("beta"))
quote["dividend_yield"] = _raw_val(detail.get("dividendYield"))
quotes.append(quote)
except Exception as e:
quotes.append({"symbol": sym, "error": str(e)})
return {"quotes": quotes}
def main():
parser = argparse.ArgumentParser(description="Fetch stock quotes")
parser.add_argument("symbols", nargs="+", help="Stock symbols")
args = parser.parse_args()
result = fetch_quote([s.upper() for s in args.symbols])
print(json.dumps(result, indent=2, default=str))
if __name__ == "__main__":
main()