
Financial Data Collector
- 648 installs
- 1.3k repo stars
- Updated August 4, 2026
- daymade/claude-code-skills
financial-data-collector is a Claude skill that pulls structured financial market and statement data for US public companies from yfinance so developers can feed DCF, comps, and earnings models.
About
financial-data-collector is a Claude Code skill that collects real financial data for any US publicly traded company from free public sources via yfinance. Output is structured JSON covering market data (price, shares, beta), historical income statements, cash flow, balance sheets, WACC inputs, and analyst estimates. Downstream skills consume the JSON for DCF modeling, comps analysis, and earnings review. Developers reach for financial-data-collector when a ticker symbol is given and clean inputs are needed before building valuation spreadsheets or automated finance agents—triggered by phrases like collect data for ticker, get financials for company, or gather DCF inputs.
- Pulls price, shares, beta, income statement, cash flow, balance sheet, WACC inputs and analyst estimates
- Outputs standardized JSON with mandatory _source attribution on every section
- Enforces NO FALLBACK values rule – missing fields are null with explicit missing source
- Preserves original yfinance CapEx sign convention and flags FCF calculation differences
- Designed as the first step in any financial analysis agent workflow
Financial Data Collector by the numbers
- 648 all-time installs (skills.sh)
- Ranked #406 of 2,064 Data Science & ML skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/daymade/claude-code-skills --skill financial-data-collectorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 648 |
|---|---|
| repo stars | ★ 1.3k |
| Last updated | August 4, 2026 |
| Repository | daymade/claude-code-skills ↗ |
How do you collect financial data for a stock ticker?
Pull clean, structured financial market and statement data for any US public company before running valuation, comps, or earnings models.
Who is it for?
Developers or analysts building DCF, comps, or earnings workflows who need clean yfinance JSON for US tickers.
Skip if: Private companies, non-US listings without yfinance coverage, or real-time trading systems requiring exchange-direct feeds.
When should I use this skill?
The user requests financial data, market data, financials, DCF inputs, or stock data for a US public company ticker.
What you get
Structured JSON with market data, income statement, cash flow, balance sheet, WACC inputs, and analyst estimates.
- structured financial JSON
- market and statement datasets
Files
Financial Data Collector
Collect and validate real financial data for US public companies using free data sources. Output is a standardized JSON file ready for consumption by other financial skills.
Critical Constraints
NO FALLBACK values. If a field cannot be retrieved, set it to null with _source: "missing". Never substitute defaults (e.g., beta or 1.0). The downstream skill decides how to handle missing data.
Data source attribution is mandatory. Every data section must have a _source field.
CapEx sign convention: yfinance returns CapEx as negative (cash outflow). Preserve the original sign. Document the convention in output metadata. Do NOT flip signs.
yfinance FCF ≠ Investment bank FCF. yfinance FCF = Operating CF + CapEx (no SBC deduction). Flag this in output metadata so downstream DCF skills don't overstate FCF.
Workflow
Step 1: Collect Data
Run the collection script:
python scripts/collect_data.py TICKER [--years 5] [--output path/to/output.json]The script collects in this priority: 1. yfinance — market data, historical financials, beta, analyst estimates 2. yfinance ^TNX — 10Y Treasury yield as risk-free rate proxy 3. User supplement — for years where yfinance returns NaN (report to user, do not guess)
Step 2: Validate Data
python scripts/validate_data.py path/to/output.jsonChecks: field completeness, cross-field consistency (Market Cap = Price × Shares), range sanity (WACC 5-20%, beta 0.3-3.0), sign conventions.
Step 3: Deliver JSON
Single file: {TICKER}_financial_data.json. Schema in references/output-schema.md.
Do NOT create: README, CSV, summary reports, or any auxiliary files.
Output Schema (Summary)
{
"ticker": "META",
"company_name": "Meta Platforms, Inc.",
"data_date": "2026-03-02",
"currency": "USD",
"unit": "millions_usd",
"data_sources": { "market_data": "...", "2022_to_2024": "..." },
"market_data": { "current_price": 648.18, "shares_outstanding_millions": 2187, "market_cap_millions": 1639607, "beta_5y_monthly": 1.284 },
"income_statement": { "2024": { "revenue": 164501, "ebit": 69380, "tax_expense": ..., "net_income": ..., "_source": "yfinance" } },
"cash_flow": { "2024": { "operating_cash_flow": ..., "capex": -37256, "depreciation_amortization": 15498, "free_cash_flow": ..., "change_in_nwc": ..., "_source": "yfinance" } },
"balance_sheet": { "2024": { "total_debt": 30768, "cash_and_equivalents": 77815, "net_debt": -47047, "current_assets": ..., "current_liabilities": ..., "_source": "yfinance" } },
"wacc_inputs": { "risk_free_rate": 0.0396, "beta": 1.284, "credit_rating": null, "_source": "yfinance + ^TNX" },
"analyst_estimates": { "revenue_next_fy": 251113, "revenue_fy_after": 295558, "eps_next_fy": 29.59, "_source": "yfinance" },
"metadata": { "_capex_convention": "negative = cash outflow", "_fcf_note": "yfinance FCF = OperatingCF + CapEx. Does NOT deduct SBC." }
}Full schema with all field definitions: references/output-schema.md
<correct_patterns>
Handling Missing Years
if pd.isna(revenue):
result[year] = {"revenue": None, "_source": "yfinance returned NaN — supplement from 10-K"}
# Report missing years to the user. Do NOT skip or fill with estimates.CapEx Sign Preservation
capex = cash_flow.loc["Capital Expenditure", year_col] # -37256.0
result["capex"] = float(capex) # Preserve negativeDatetime Column Indexing
year_col = [c for c in financials.columns if c.year == target_year][0]
revenue = financials.loc["Total Revenue", year_col]Field Name Guards
if "Total Revenue" in financials.index:
revenue = financials.loc["Total Revenue", year_col]
elif "Revenue" in financials.index:
revenue = financials.loc["Revenue", year_col]
else:
revenue = None</correct_patterns>
<common_mistakes>
Mistake 1: Default Values for Missing Data
# ❌ WRONG
beta = info.get("beta", 1.0)
growth = data.get("growth") or 0.02
# ✅ RIGHT
beta = info.get("beta") # May be None — that's OKMistake 2: Assuming All Years Have Data
# ❌ WRONG — 2020-2021 may be NaN
revenue = float(financials.loc["Total Revenue", year_col])
# ✅ RIGHT
value = financials.loc["Total Revenue", year_col]
revenue = float(value) if pd.notna(value) else NoneMistake 3: Using yfinance FCF in DCF Models Directly
yfinance FCF does NOT deduct SBC. For mega-caps like META, SBC can be $20-30B/yr, making yfinance FCF ~30% higher than investment-bank FCF. Always flag this in output.
Mistake 4: Flipping CapEx Sign
# ❌ WRONG — double-negation risk downstream
capex = abs(cash_flow.loc["Capital Expenditure", year_col])
# ✅ RIGHT — preserve original, document convention
capex = float(cash_flow.loc["Capital Expenditure", year_col]) # -37256.0</common_mistakes>
Known yfinance Pitfalls
See references/yfinance-pitfalls.md for detailed field mapping and workarounds.
# Python cache
__pycache__/
*.pyc
*.pyo
# Test outputs
*_financial_data.json
Security scan passed
Scanned at: 2026-06-13T19:44:42.093312
Tool: gitleaks + pattern-based validation
Content hash: 191228b4d56b8c3814596c77dd1ac78d6d1ac25ce4b2eb81db3d487ebef49a2b
Output Schema — financial-data-collector
All monetary values in millions USD unless noted. Null means "not available from source."
Top-Level Fields
| Field | Type | Description |
|---|---|---|
ticker | string | Stock ticker symbol (e.g., "META") |
company_name | string | Full company name from yfinance |
data_date | string | ISO date when data was collected |
currency | string | Always "USD" for US equities |
unit | string | Always "millions_usd" |
data_sources | object | Attribution for each data section |
market_data
| Field | Type | Source | Notes |
|---|---|---|---|
current_price | float | yf.Ticker.info["currentPrice"] | USD per share |
shares_outstanding_millions | float | info["sharesOutstanding"] / 1e6 | Diluted if available |
market_cap_millions | float | info["marketCap"] / 1e6 | |
beta_5y_monthly | float\ | null | info["beta"] |
income_statement (keyed by year string)
| Field | Type | Source Row Index |
|---|---|---|
revenue | float\ | null |
ebit | float\ | null |
ebitda | float\ | null |
tax_expense | float\ | null |
net_income | float\ | null |
sbc | float\ | null |
_source | string | Data provenance |
cash_flow (keyed by year string)
| Field | Type | Source Row Index | Sign |
|---|---|---|---|
operating_cash_flow | float\ | null | "Operating Cash Flow" |
capex | float\ | null | "Capital Expenditure" |
depreciation_amortization | float\ | null | "Depreciation And Amortization" |
free_cash_flow | float\ | null | "Free Cash Flow" |
change_in_nwc | float\ | null | "Change In Working Capital" |
_source | string |
balance_sheet (latest year only by default)
| Field | Type | Source Row Index |
|---|---|---|
total_debt | float\ | null |
cash_and_equivalents | float\ | null |
net_debt | float\ | null |
current_assets | float\ | null |
current_liabilities | float\ | null |
total_assets | float\ | null |
total_equity | float\ | null |
_source | string |
wacc_inputs
| Field | Type | Source |
|---|---|---|
risk_free_rate | float\ | null |
beta | float\ | null |
credit_rating | string\ | null |
analyst_estimates
| Field | Type | Source |
|---|---|---|
revenue_next_fy | float\ | null |
revenue_fy_after | float\ | null |
eps_next_fy | float\ | null |
metadata
| Field | Value |
|---|---|
_capex_convention | "negative = cash outflow (yfinance convention)" |
_fcf_note | "yfinance FCF = OperatingCF + CapEx. Does NOT deduct SBC." |
_nan_years | List of years where yfinance returned NaN |
_collection_duration_seconds | Time taken to collect all data |
yfinance Pitfalls & Field Mapping
NaN Year Patterns
yfinance frequently returns NaN for older fiscal years. Observed patterns:
| Ticker | NaN Years | Notes |
|---|---|---|
| META | 2020, 2021 | All fields NaN; must supplement from 10-K |
| General | Varies | Older years (>3 years back) are less reliable |
Workaround: Check every field with pd.notna(). Report NaN years to user. Never fill with estimates.
Field Name Variants
yfinance row index names are not fully stable across versions. Use fallback chains:
FIELD_ALIASES = {
"revenue": ["Total Revenue", "Revenue", "Operating Revenue"],
"ebit": ["Operating Income", "EBIT"],
"ebitda": ["EBITDA", "Normalized EBITDA"],
"tax": ["Tax Provision", "Income Tax Expense", "Tax Effect Of Unusual Items"],
"net_income": ["Net Income", "Net Income Common Stockholders"],
"capex": ["Capital Expenditure", "Capital Expenditures"],
"ocf": ["Operating Cash Flow", "Cash Flow From Continuing Operating Activities"],
"da": ["Depreciation And Amortization", "Depreciation Amortization Depletion"],
"fcf": ["Free Cash Flow"],
"nwc": ["Change In Working Capital", "Changes In Working Capital"],
"total_debt": ["Total Debt"],
"cash": ["Cash And Cash Equivalents"],
"short_investments": ["Other Short Term Investments", "Short Term Investments"],
"sbc": ["Stock Based Compensation"],
}
def safe_get(df, aliases, col):
for alias in aliases:
if alias in df.index:
val = df.loc[alias, col]
return float(val) if pd.notna(val) else None
return NoneDatetime Column Index
yfinance returns DataFrame columns as pandas.Timestamp, not integer years:
# ❌ WRONG
financials[2024] # KeyError
# ✅ RIGHT
year_col = [c for c in financials.columns if c.year == 2024][0]
financials.loc["Total Revenue", year_col]Shares Outstanding Variants
# Preferred: diluted
shares = info.get("sharesOutstanding") # Basic shares
# Alternative
shares = info.get("impliedSharesOutstanding") # May be more accurateRisk-Free Rate via ^TNX
tnx = yf.Ticker("^TNX")
hist = tnx.history(period="1d")
risk_free_rate = hist["Close"].iloc[-1] / 100 # Convert from percentagePitfall: ^TNX returns yield as percentage (e.g., 4.3), not decimal (0.043). Divide by 100.
Analyst Estimates
ticker = yf.Ticker("META")
# Revenue estimates
rev_est = ticker.revenue_estimate # DataFrame with columns: avg, low, high, ...
# Rows: "0q" (current quarter), "+1q", "0y" (current year), "+1y"
# EPS estimates
eps_est = ticker.eps_trend # Similar structurePitfall: These APIs change between yfinance versions. Always wrap in try/except.
FCF Definition Mismatch
| Source | FCF Definition | META 2024 |
|---|---|---|
| yfinance | Operating CF + CapEx | ~$54.1B |
| Morgan Stanley DCF | EBITDA - Taxes - CapEx - NWC - SBC | ~$37.9B |
| Difference | SBC (~$22B) + other adjustments | ~30% gap |
Always flag this in output metadata. Downstream DCF skills need to decide whether to use yfinance FCF or reconstruct from components.
#!/usr/bin/env python3
"""
Collect real financial data for a US publicly traded company.
Output: structured JSON consumable by downstream financial skills.
Usage:
python collect_data.py TICKER [--years 5] [--output path/to/output.json]
Examples:
python collect_data.py META
python collect_data.py AAPL --years 3 --output /tmp/aapl_data.json
python collect_data.py NVDA --years 5
Dependencies: yfinance, pandas (via uv inline or pip)
"""
# /// script
# requires-python = ">=3.11"
# dependencies = ["yfinance>=0.2.0", "pandas>=2.0.0"]
# ///
import argparse
import json
import sys
import time
from datetime import date
from pathlib import Path
import pandas as pd
import yfinance as yf
# Field name aliases — yfinance row indices vary across versions
FIELD_ALIASES = {
"revenue": ["Total Revenue", "Revenue", "Operating Revenue"],
"ebit": ["Operating Income", "EBIT"],
"ebitda": ["EBITDA", "Normalized EBITDA"],
"tax_expense": ["Tax Provision", "Income Tax Expense"],
"net_income": ["Net Income", "Net Income Common Stockholders"],
"capex": ["Capital Expenditure", "Capital Expenditures"],
"operating_cash_flow": ["Operating Cash Flow", "Cash Flow From Continuing Operating Activities"],
"depreciation_amortization": ["Depreciation And Amortization", "Depreciation Amortization Depletion"],
"free_cash_flow": ["Free Cash Flow"],
"change_in_nwc": ["Change In Working Capital", "Changes In Working Capital"],
"sbc": ["Stock Based Compensation"],
"total_debt": ["Total Debt"],
"long_term_debt": ["Long Term Debt"],
"short_term_debt": ["Short Long Term Debt", "Current Debt"],
"cash": ["Cash And Cash Equivalents"],
"short_investments": ["Other Short Term Investments", "Short Term Investments"],
"current_assets": ["Current Assets"],
"current_liabilities": ["Current Liabilities"],
"total_assets": ["Total Assets"],
"total_equity": ["Stockholders Equity", "Total Equity Gross Minority Interest"],
}
def safe_get(df: pd.DataFrame, field_key: str, col) -> float | None:
"""Safely extract a value from a DataFrame using alias chain."""
aliases = FIELD_ALIASES.get(field_key, [field_key])
for alias in aliases:
if alias in df.index:
val = df.loc[alias, col]
if pd.notna(val):
return float(val)
return None
def get_year_col(df: pd.DataFrame, year: int):
"""Find the column matching a target year in a yfinance DataFrame."""
matches = [c for c in df.columns if c.year == year]
return matches[0] if matches else None
def collect_market_data(ticker_obj: yf.Ticker) -> dict:
"""Collect real-time market data."""
info = ticker_obj.info
price = info.get("currentPrice") or info.get("regularMarketPrice")
shares_raw = info.get("sharesOutstanding")
shares = shares_raw / 1e6 if shares_raw else None
mcap_raw = info.get("marketCap")
mcap = mcap_raw / 1e6 if mcap_raw else None
beta = info.get("beta")
return {
"current_price": price,
"shares_outstanding_millions": round(shares, 2) if shares else None,
"market_cap_millions": round(mcap, 2) if mcap else None,
"beta_5y_monthly": round(beta, 3) if beta else None,
}
def collect_risk_free_rate() -> float | None:
"""Get 10Y Treasury yield as risk-free rate proxy."""
try:
tnx = yf.Ticker("^TNX")
hist = tnx.history(period="5d")
if hist.empty:
return None
yield_pct = hist["Close"].iloc[-1]
return round(float(yield_pct) / 100, 4) # Convert percentage to decimal
except Exception:
return None
def collect_income_statement(ticker_obj: yf.Ticker, years: list[int]) -> dict:
"""Collect income statement data for specified years."""
try:
financials = ticker_obj.financials
except Exception:
return {str(y): {"_source": "yfinance error"} for y in years}
if financials is None or financials.empty:
return {str(y): {"_source": "yfinance returned empty"} for y in years}
result = {}
nan_years = []
for year in years:
col = get_year_col(financials, year)
if col is None:
result[str(year)] = {"_source": f"yfinance has no column for {year}"}
nan_years.append(year)
continue
revenue = safe_get(financials, "revenue", col)
ebit = safe_get(financials, "ebit", col)
if revenue is None and ebit is None:
nan_years.append(year)
result[str(year)] = {
"revenue": None, "ebit": None, "ebitda": None,
"tax_expense": None, "net_income": None,
"_source": f"yfinance returned NaN for {year} — supplement from 10-K filing",
}
else:
result[str(year)] = {
"revenue": revenue,
"ebit": ebit,
"ebitda": safe_get(financials, "ebitda", col),
"tax_expense": safe_get(financials, "tax_expense", col),
"net_income": safe_get(financials, "net_income", col),
"_source": "yfinance",
}
return result, nan_years
def collect_cash_flow(ticker_obj: yf.Ticker, years: list[int]) -> dict:
"""Collect cash flow data."""
try:
cf = ticker_obj.cashflow
except Exception:
return {str(y): {"_source": "yfinance error"} for y in years}, []
if cf is None or cf.empty:
return {str(y): {"_source": "yfinance returned empty"} for y in years}, years
result = {}
nan_years = []
for year in years:
col = get_year_col(cf, year)
if col is None:
result[str(year)] = {"_source": f"yfinance has no column for {year}"}
nan_years.append(year)
continue
ocf = safe_get(cf, "operating_cash_flow", col)
capex = safe_get(cf, "capex", col)
if ocf is None and capex is None:
nan_years.append(year)
result[str(year)] = {
"operating_cash_flow": None, "capex": None,
"depreciation_amortization": None, "free_cash_flow": None,
"change_in_nwc": None, "sbc": None,
"_source": f"yfinance returned NaN for {year}",
}
else:
result[str(year)] = {
"operating_cash_flow": ocf,
"capex": capex, # Negative = outflow (preserved)
"depreciation_amortization": safe_get(cf, "depreciation_amortization", col),
"free_cash_flow": safe_get(cf, "free_cash_flow", col),
"change_in_nwc": safe_get(cf, "change_in_nwc", col),
"sbc": safe_get(cf, "sbc", col),
"_source": "yfinance",
}
return result, nan_years
def collect_balance_sheet(ticker_obj: yf.Ticker, latest_year: int) -> dict:
"""Collect balance sheet data for the latest year."""
try:
bs = ticker_obj.balance_sheet
except Exception:
return {str(latest_year): {"_source": "yfinance error"}}
if bs is None or bs.empty:
return {str(latest_year): {"_source": "yfinance returned empty"}}
col = get_year_col(bs, latest_year)
if col is None:
return {str(latest_year): {"_source": f"yfinance has no column for {latest_year}"}}
total_debt = safe_get(bs, "total_debt", col)
if total_debt is None:
lt = safe_get(bs, "long_term_debt", col) or 0
st = safe_get(bs, "short_term_debt", col) or 0
total_debt = lt + st if (lt or st) else None
cash = safe_get(bs, "cash", col)
short_inv = safe_get(bs, "short_investments", col)
cash_equiv = (cash or 0) + (short_inv or 0) if (cash is not None or short_inv is not None) else None
net_debt = (total_debt - cash_equiv) if (total_debt is not None and cash_equiv is not None) else None
return {
str(latest_year): {
"total_debt": total_debt,
"cash_and_equivalents": cash_equiv,
"net_debt": round(net_debt, 2) if net_debt is not None else None,
"current_assets": safe_get(bs, "current_assets", col),
"current_liabilities": safe_get(bs, "current_liabilities", col),
"total_assets": safe_get(bs, "total_assets", col),
"total_equity": safe_get(bs, "total_equity", col),
"_source": "yfinance",
}
}
def collect_analyst_estimates(ticker_obj: yf.Ticker) -> dict:
"""Collect analyst consensus estimates."""
result = {"revenue_next_fy": None, "revenue_fy_after": None, "eps_next_fy": None, "_source": "missing"}
try:
rev_est = ticker_obj.revenue_estimate
if rev_est is not None and not rev_est.empty:
if "0y" in rev_est.index:
result["revenue_next_fy"] = float(rev_est.loc["0y", "avg"]) / 1e6 if pd.notna(rev_est.loc["0y", "avg"]) else None
if "+1y" in rev_est.index:
result["revenue_fy_after"] = float(rev_est.loc["+1y", "avg"]) / 1e6 if pd.notna(rev_est.loc["+1y", "avg"]) else None
result["_source"] = "yfinance revenue_estimate"
except Exception:
pass
try:
eps = ticker_obj.eps_trend
if eps is not None and not eps.empty:
if "0y" in eps.index:
val = eps.loc["0y", "current"] if "current" in eps.columns else eps.iloc[eps.index.get_loc("0y"), 0]
result["eps_next_fy"] = float(val) if pd.notna(val) else None
except Exception:
pass
return result
def main():
parser = argparse.ArgumentParser(description="Collect financial data for a US public company")
parser.add_argument("ticker", help="Stock ticker symbol (e.g., META, AAPL)")
parser.add_argument("--years", type=int, default=5, help="Number of historical years (default: 5)")
parser.add_argument("--output", help="Output JSON file path (default: {TICKER}_financial_data.json)")
args = parser.parse_args()
ticker_symbol = args.ticker.upper()
current_year = date.today().year
# Exclude current year if annual report not yet filed (typically before Q4 earnings)
latest_full_year = current_year - 1
target_years = list(range(latest_full_year - args.years + 1, latest_full_year + 1))
output_path = args.output or f"{ticker_symbol}_financial_data.json"
print(f"Collecting data for {ticker_symbol} ({target_years[0]}-{target_years[-1]})...")
start_time = time.time()
ticker_obj = yf.Ticker(ticker_symbol)
# Verify ticker is valid
info = ticker_obj.info
if not info or info.get("regularMarketPrice") is None:
print(f"ERROR: {ticker_symbol} not found or no market data available", file=sys.stderr)
sys.exit(1)
company_name = info.get("longName") or info.get("shortName") or ticker_symbol
# Collect all data
print(" Market data...", end=" ", flush=True)
market_data = collect_market_data(ticker_obj)
print("OK")
print(" Risk-free rate...", end=" ", flush=True)
rfr = collect_risk_free_rate()
print(f"{rfr:.4f}" if rfr else "MISSING")
print(" Income statement...", end=" ", flush=True)
income_data, is_nan_years = collect_income_statement(ticker_obj, target_years)
print(f"OK (NaN years: {is_nan_years or 'none'})")
print(" Cash flow...", end=" ", flush=True)
cf_data, cf_nan_years = collect_cash_flow(ticker_obj, target_years)
print(f"OK (NaN years: {cf_nan_years or 'none'})")
print(" Balance sheet...", end=" ", flush=True)
bs_data = collect_balance_sheet(ticker_obj, latest_full_year)
print("OK")
print(" Analyst estimates...", end=" ", flush=True)
estimates = collect_analyst_estimates(ticker_obj)
print("OK" if estimates["_source"] != "missing" else "PARTIAL/MISSING")
elapsed = round(time.time() - start_time, 1)
all_nan_years = sorted(set(is_nan_years + cf_nan_years))
# Assemble output
output = {
"ticker": ticker_symbol,
"company_name": company_name,
"data_date": date.today().isoformat(),
"currency": "USD",
"unit": "millions_usd",
"data_sources": {
"market_data": "yfinance (live)",
"historical_financials": "yfinance annual financials",
"risk_free_rate": "yfinance ^TNX (10Y Treasury)",
},
"market_data": market_data,
"income_statement": income_data,
"cash_flow": cf_data,
"balance_sheet": bs_data,
"wacc_inputs": {
"risk_free_rate": rfr,
"beta": market_data.get("beta_5y_monthly"),
"credit_rating": None,
"_source": "yfinance + ^TNX",
},
"analyst_estimates": estimates,
"metadata": {
"_capex_convention": "negative = cash outflow (yfinance convention)",
"_fcf_note": "yfinance FCF = OperatingCF + CapEx. Does NOT deduct SBC.",
"_nan_years": all_nan_years,
"_collection_duration_seconds": elapsed,
"_target_years": target_years,
},
}
# Write output
Path(output_path).write_text(json.dumps(output, indent=2, ensure_ascii=False))
print(f"\nSaved to {output_path} ({elapsed}s)")
# Summary
if all_nan_years:
print(f"\n⚠️ WARNING: Years {all_nan_years} have missing data (NaN from yfinance).")
print(" Supplement from 10-K filings or SEC EDGAR before using in models.")
# Quick sanity check
if market_data["current_price"] and market_data["shares_outstanding_millions"]:
computed_mcap = market_data["current_price"] * market_data["shares_outstanding_millions"]
reported_mcap = market_data["market_cap_millions"]
if reported_mcap and abs(computed_mcap - reported_mcap) / reported_mcap > 0.05:
print(f"⚠️ Market cap mismatch: Price×Shares={computed_mcap:.0f}M vs Reported={reported_mcap:.0f}M")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Validate financial data JSON output from collect_data.py.
Checks completeness, consistency, and sanity of collected data.
Usage:
python validate_data.py path/to/output.json
Returns JSON validation report to stdout.
"""
# /// script
# requires-python = ">=3.11"
# dependencies = []
# ///
import json
import sys
from pathlib import Path
def validate(data: dict) -> dict:
"""Validate financial data JSON. Returns validation report."""
errors = []
warnings = []
# 1. Required top-level fields
for field in ["ticker", "company_name", "data_date", "market_data",
"income_statement", "cash_flow", "balance_sheet", "wacc_inputs"]:
if field not in data:
errors.append(f"Missing required field: {field}")
if errors:
return {"status": "error", "errors": errors, "warnings": warnings}
md = data["market_data"]
# 2. Market data sanity
if md.get("current_price") is not None:
if md["current_price"] <= 0:
errors.append(f"Invalid stock price: {md['current_price']}")
if md["current_price"] > 10000:
warnings.append(f"Unusually high stock price: ${md['current_price']}")
if md.get("shares_outstanding_millions") is not None:
if md["shares_outstanding_millions"] <= 0:
errors.append(f"Invalid shares outstanding: {md['shares_outstanding_millions']}")
if md.get("beta_5y_monthly") is not None:
beta = md["beta_5y_monthly"]
if beta < 0.1 or beta > 5.0:
warnings.append(f"Unusual beta: {beta} (expected 0.3-3.0)")
# 3. Market cap cross-check
if md.get("current_price") and md.get("shares_outstanding_millions") and md.get("market_cap_millions"):
computed = md["current_price"] * md["shares_outstanding_millions"]
reported = md["market_cap_millions"]
pct_diff = abs(computed - reported) / reported
if pct_diff > 0.05:
# yfinance sharesOutstanding is basic; marketCap may use diluted. Known discrepancy.
warnings.append(f"Market cap mismatch ({pct_diff:.1%}): Price×Shares(basic)={computed:.0f}M vs Reported={reported:.0f}M. Likely basic vs diluted shares.")
# 4. Income statement completeness
is_data = data.get("income_statement", {})
years_with_data = 0
for year, vals in is_data.items():
if isinstance(vals, dict) and vals.get("revenue") is not None:
years_with_data += 1
# Revenue should be positive
if vals["revenue"] <= 0:
warnings.append(f"Non-positive revenue in {year}: {vals['revenue']}")
# EBIT margin sanity
if vals.get("ebit") is not None and vals["revenue"] > 0:
margin = vals["ebit"] / vals["revenue"]
if margin < -1.0 or margin > 0.8:
warnings.append(f"Unusual EBIT margin in {year}: {margin:.1%}")
if years_with_data == 0:
errors.append("No income statement data available for any year")
elif years_with_data < 3:
warnings.append(f"Only {years_with_data} years of income statement data (recommend ≥3)")
# 5. Cash flow: CapEx sign convention
cf_data = data.get("cash_flow", {})
for year, vals in cf_data.items():
if isinstance(vals, dict) and vals.get("capex") is not None:
if vals["capex"] > 0:
warnings.append(f"CapEx is positive in {year} ({vals['capex']}). Expected negative (outflow).")
# 6. Balance sheet: Net debt consistency
bs_data = data.get("balance_sheet", {})
for year, vals in bs_data.items():
if isinstance(vals, dict):
td = vals.get("total_debt")
ce = vals.get("cash_and_equivalents")
nd = vals.get("net_debt")
if td is not None and ce is not None and nd is not None:
expected_nd = td - ce
if abs(expected_nd - nd) > 1.0: # Allow $1M rounding
errors.append(f"Net debt inconsistency in {year}: total_debt({td}) - cash({ce}) = {expected_nd} ≠ {nd}")
# 7. WACC inputs
wacc = data.get("wacc_inputs", {})
rfr = wacc.get("risk_free_rate")
if rfr is not None:
if rfr < 0 or rfr > 0.15:
warnings.append(f"Unusual risk-free rate: {rfr:.2%} (expected 1-8%)")
else:
warnings.append("Risk-free rate is missing")
# 8. NaN years tracking
meta = data.get("metadata", {})
nan_years = meta.get("_nan_years", [])
if nan_years:
warnings.append(f"NaN years detected: {nan_years}. Supplement from 10-K before using in models.")
# 9. Data source attribution
for section in ["income_statement", "cash_flow", "balance_sheet"]:
section_data = data.get(section, {})
for year, vals in section_data.items():
if isinstance(vals, dict) and "_source" not in vals:
warnings.append(f"Missing _source attribution in {section}.{year}")
status = "error" if errors else ("warning" if warnings else "success")
return {
"status": status,
"ticker": data.get("ticker"),
"years_with_data": years_with_data,
"errors": errors,
"warnings": warnings,
"error_count": len(errors),
"warning_count": len(warnings),
}
def main():
if len(sys.argv) < 2:
print("Usage: python validate_data.py <json_file>", file=sys.stderr)
sys.exit(1)
json_path = sys.argv[1]
if not Path(json_path).exists():
print(json.dumps({"status": "error", "errors": [f"File not found: {json_path}"]}))
sys.exit(1)
data = json.loads(Path(json_path).read_text())
report = validate(data)
print(json.dumps(report, indent=2))
if report["status"] == "error":
sys.exit(1)
elif report["status"] == "warning":
sys.exit(0) # Warnings are OK, just informational
else:
sys.exit(0)
if __name__ == "__main__":
main()
Related skills
How it compares
Use financial-data-collector for quick free yfinance JSON inputs instead of wiring a paid market-data API for exploratory valuation work.
FAQ
What data does financial-data-collector return?
financial-data-collector outputs structured JSON with market data (price, shares, beta), historical income statement, cash flow, balance sheet figures, WACC inputs, and analyst estimates from yfinance.
Which companies does financial-data-collector support?
financial-data-collector targets US publicly traded companies, pulling from free public sources via yfinance before downstream DCF, comps, or earnings analysis skills run.