Skip to content

How I Use SEC EDGAR to Compare Companies Instead of Guessing (Part2)

Jul 20, 2026 25 min read 0 comments

Table of Contents

Part 2 of 2, Turning Filings into Data with Python

This is Part 2 of two. Part 1 covered what EDGAR is, the filings that matter, and the questions I ask. If you have not read it, start there, the code below only makes sense once you know what you are looking for and why.

In Part 1 I walked through a seven-step way of looking at a company:

  1. understand the business
  2. revenue growth
  3. gross margin
  4. operating margin
  5. free cash flow
  6. debt and cash
  7. the risk factors.

Reading filings by hand is essential, but once I want to compare companies on those same metrics, doing it by hand gets slow to impossible and this is where Python comes in.

Using Python With EDGAR

Reading filings manually is essential and in my view its a must exercise, but Python helps when I want to compare numbers across companies. The SEC exposes structured company data through a free API, so I can write scripts to fetch revenue, gross profit, operating income, net income, cash flow, assets, liabilities, and more, then run the same process across companies again and again.

The main goal here is not to automate thinking, the goal is to automate data collection so I have more time to think.

A Note Before Running Anything

The SEC expects automated requests to include a proper, descriptive User-Agent with contact information. Do not use a fake or empty one, the server returns an error. Use something that identifies your project, like:

USER_AGENT = "<your_name> research your-email@example.com"

And yes, I should not forget, do not spam the servers. This is research data, not high-frequency trading data; reasonable delays are part of being a good citizen. The toolkit below is built around exactly this principle: download each company once, then analyze it locally as many times as you want.

One small note, the GitHub repository includes more detailed comments compared to the script below, which I’ve tried to keep them as concise as possible for this article.”/span>

The Python Toolkit

The scripts map one-to-one onto the seven-step workflow from Part 1, so each analysis step has matching code. They all read from a single cached download, and every metric is produced in both annual and quarterly form so I can do the year-over-year comparisons that seasonality demands.

Script Analysis step What it produces
01_download_company_facts.py Step 0 (foundation) Caches each company’s full companyfacts JSON locally
02_revenue_gross_profit.py Steps 2 & 3 Revenue + gross margin, annual & quarterly, to CSV
03_compare_visual.py Step 3 Charts two companies side by side
04_revenue_growth.py Step 2 Year-over-year revenue growth, annual & quarterly
05_net_income_margin.py (profitability) Net income + net margin
06_operating_margin.py Step 4 Operating income + operating margin
07_free_cash_flow.py Step 5 Operating cash flow, capex, free cash flow
08_cash_vs_debt.py Step 6 Cash vs. (estimated) total debt
08_cash_vs_debt.py Step 6 Cash vs. (estimated) total debt

A few design choices worth calling out:

  • Download once, analyze many The companyfacts endpoint returns every tagged concept for a company in one JSON file. Script 1 caches it; every other script reads the cache. That keeps the load on SEC’s servers minimal.
  • Annual and quarterly Companies do not file a standalone Q4, so the toolkit derives it (full year from the 10-K minus the nine-month figure from the Q3 10-Q). Quarterly growth is always year-over-year, never quarter-over-quarter, so seasonality does not distort it.
  • The debt number is an estimate XBRL debt tagging is genuinely inconsistent, so Script 8 labels its figure total_debt_est and tells you to confirm it against the balance sheet, that honesty is the point.

The engine: edgar_utils.py

Everything shared, ticker lookup, downloading and caching, turning raw facts into clean annual/quarterly series, deriving Q4, margins, year-over-year growth, CSV output, and charts lives here.

"""
edgar_utils.py --- shared engine for the EDGAR toolkit.

Philosophy: 
    - DOWNLOAD ONCE (Script 1) 
    - ANALYZE MANY TIMES (Scripts 2-8)
REQUIREMENTS:  pip install requests matplotlib
IMPORTANT:     edit USER_AGENT below to your real name + email.
"""

import csv
import json
import os
import time
from datetime import date

import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt
import requests

# --- EDIT THIS -------------------------------------------------------------
USER_AGENT = "<your_name> research your-email@example.com"
# ---------------------------------------------------------------------------

HEADERS = {"User-Agent": USER_AGENT}
CACHE_DIR = "edgar_cache"
OUT_DIR = "edgar_output"
os.makedirs(CACHE_DIR, exist_ok=True)
os.makedirs(OUT_DIR, exist_ok=True)

# Revenue has been tagged under different concepts over the years; try in order.
REVENUE_TAGS = [
    "RevenueFromContractWithCustomerExcludingAssessedTax",
    "Revenues",
    "SalesRevenueNet",
]

plt.rcParams.update({
    "figure.dpi": 150, "savefig.dpi": 150, "font.size": 12,
    "axes.spines.top": False, "axes.spines.right": False,
    "axes.grid": True, "grid.alpha": 0.25,
})

# --- Ticker -> CIK ---------------------------------------------------------
_TICKER_MAP = None


def cik_for(ticker):
    """Resolve a ticker like 'V' to SEC's 10-digit zero-padded CIK."""
    global _TICKER_MAP
    if _TICKER_MAP is None:
        url = "https://www.sec.gov/files/company_tickers.json"
        rows = requests.get(url, headers=HEADERS, timeout=30).json()
        _TICKER_MAP = {r["ticker"].upper(): str(r["cik_str"]).zfill(10)
                       for r in rows.values()}
        time.sleep(0.3)
    t = ticker.upper()
    if t not in _TICKER_MAP:
        raise ValueError(f"Ticker {ticker!r} not found in SEC's ticker list.")
    return _TICKER_MAP[t]


# --- Download + cache (the only functions that touch the network) ----------
def _cache_path(ticker):
    return os.path.join(CACHE_DIR, f"{ticker.upper()}_{cik_for(ticker)}.json")


def download_company_facts(ticker, refresh=False):
    """Download the full companyfacts JSON for a ticker and cache it."""
    path = _cache_path(ticker)
    if os.path.exists(path) and not refresh:
        with open(path) as f:
            return json.load(f)
    url = f"https://data.sec.gov/api/xbrl/companyfacts/CIK{cik_for(ticker)}.json"
    resp = requests.get(url, headers=HEADERS, timeout=60)
    resp.raise_for_status()
    data = resp.json()
    with open(path, "w") as f:
        json.dump(data, f)
    time.sleep(0.3)
    return data


def load_facts(ticker):
    """Load cached facts; download on first use."""
    path = _cache_path(ticker)
    if not os.path.exists(path):
        return download_company_facts(ticker)
    with open(path) as f:
        return json.load(f)


# --- Raw facts -> clean series (the fiddly part) ---------------------------
def _usd_facts(facts, tag):
    try:
        return facts["facts"]["us-gaap"][tag]["units"]["USD"]
    except KeyError:
        return []


def _days(f):
    return (date.fromisoformat(f["end"]) - date.fromisoformat(f["start"])).days


def _flow_one_tag(facts, tag):
    """For ONE flow tag, return (annual {end:val}, quarterly {end:val})."""
    annual_by_end = {}
    annual_by_start = {}
    nine_month = {}
    quarterly = {}

    for f in _usd_facts(facts, tag):
        if "start" not in f:  # instant (balance-sheet) item
            continue
        d = _days(f)
        end = date.fromisoformat(f["end"])
        start = f["start"]
        if 80 <= d <= 100:  # 3-month quarter
            quarterly[end] = f["val"]
        elif 250 <= d <= 295:  # 9-month year-to-date
            nine_month[start] = (end, f["val"])
        elif 350 <= d <= 380:  # full fiscal year
            annual_by_end[end] = f["val"]
            annual_by_start[start] = (end, f["val"])

    # Derive Q4 = full year - 9-month YTD, matched on shared fiscal-year start.
    for start, (fy_end, fy_val) in annual_by_start.items():
        if fy_end in quarterly:
            continue
        if start in nine_month:
            _, ytd9 = nine_month[start]
            quarterly[fy_end] = fy_val - ytd9

    return annual_by_end, quarterly


def flow_series(facts, tag_or_tags):
    """Flow item (revenue, profit, cash flow). One tag or a list of candidates.
    Returns (annual, quarterly), each {end_date: value} sorted."""
    tags = [tag_or_tags] if isinstance(tag_or_tags, str) else list(tag_or_tags)
    annual, quarterly = {}, {}
    for tag in tags:
        a, q = _flow_one_tag(facts, tag)
        for d, v in a.items():
            annual.setdefault(d, v)
        for d, v in q.items():
            quarterly.setdefault(d, v)
    return dict(sorted(annual.items())), dict(sorted(quarterly.items()))


def instant_series(facts, tag_or_tags):
    """Point-in-time item (cash, debt). Returns {end_date: value} sorted."""
    tags = [tag_or_tags] if isinstance(tag_or_tags, str) else list(tag_or_tags)
    out = {}
    for tag in tags:
        for f in _usd_facts(facts, tag):
            if "start" in f and f["start"] != f["end"]:
                continue
            out.setdefault(date.fromisoformat(f["end"]), f["val"])
    return dict(sorted(out.items()))


# --- Derived metrics -------------------------------------------------------
def margin(numerator, denominator):
    """Percent margin where both series have a value for the date."""
    return {d: numerator[d] / denominator[d] * 100
            for d in denominator if d in numerator and denominator[d]}


def yoy_growth(series):
    """Year-over-year % growth vs the value ~365 days earlier. Works for
    annual (one point/yr) and quarterly (Q vs same Q last year)."""
    dates = sorted(series)
    out = {}
    for d in dates:
        prior = [p for p in dates if 350 <= (d - p).days <= 380]
        if prior and series[prior[-1]]:
            out[d] = (series[d] - series[prior[-1]]) / series[prior[-1]] * 100
    return out


# --- Output ----------------------------------------------------------------
def save_csv(fname, header, rows):
    path = os.path.join(OUT_DIR, fname)
    with open(path, "w", newline="") as f:
        w = csv.writer(f)
        w.writerow(header)
        w.writerows(rows)
    print(f"  wrote {path} ({len(rows)} rows)")


def plot_compare(data, title, ylabel, fname,
                 kind="line", pct=False, billions=False):
    """data: {label: {date: value}}. Multiple series -> lines for legibility."""
    fig, ax = plt.subplots(figsize=(11, 5.6))
    fig.subplots_adjust(left=0.09, right=0.97, top=0.86, bottom=0.12)
    all_dates = sorted({d for s in data.values() for d in s})
    if not all_dates:
        print(f"  (no data to plot for {fname})")
        plt.close(fig)
        return
    idx = {d: i for i, d in enumerate(all_dates)}
    colors = ["#2563eb", "#dc2626", "#059669", "#d97706"]
    for k, (label, s) in enumerate(data.items()):
        ds = sorted(s)
        xs = [idx[d] for d in ds]
        ys = [s[d] / (1e9 if billions else 1) for d in ds]
        c = colors[k % len(colors)]
        if kind == "bar" and len(data) == 1:
            ax.bar(xs, ys, color=c, width=0.85)
        else:
            ax.plot(xs, ys, "-o", lw=2.2, ms=4, color=c, label=label)
    step = max(1, len(all_dates) // 8)
    ax.set_xticks(range(0, len(all_dates), step))
    ax.set_xticklabels([all_dates[i].year for i in range(0, len(all_dates), step)])
    ax.set_ylabel(ylabel)
    if pct:
        ax.yaxis.set_major_formatter(lambda v, _: f"{v:.0f}%")
    if len(data) > 1:
        ax.legend(frameon=False)
    fig.suptitle(title, x=0.015, ha="left", fontsize=15, fontweight="bold")
    path = os.path.join(OUT_DIR, fname)
    fig.savefig(path, facecolor="white")
    plt.close(fig)
    print(f"  wrote {path}")

Script 1 | Download Company Facts

Before I can compare anything, I need the raw data. This first script does one simple job: it downloads structured company facts from the SEC’s EDGAR API and saves them as JSON files on my computer.

This script helps me answer:

  • Can I download official SEC financial data without copying numbers manually?
  • Can I store the raw company data locally so I do not keep hitting the SEC servers?
  • Can I create a repeatable starting point for comparing several companies?

The output is not analysis yet, it is just the raw material, but that matters because every later script depends on this step.

"""01_download_company_facts.py --- Step 0: get the data once, store it locally."""
from edgar_utils import download_company_facts

# Three comparison pairs from the article (edit freely):
TICKERS = ["V", "MA",  # payments  -- the clean comparison
           "KO", "PEP",  # beverages -- similar label, different mix
           "AAPL", "MSFT"]  # mega-cap tech -- same label, different economics


def main():
    for t in TICKERS:
        print(f"Downloading {t} ...")
        facts = download_company_facts(t)  # refresh=True to force re-pull
        n = len(facts.get("facts", {}).get("us-gaap", {}))
        print(f"  cached {t}: {n} us-gaap concepts")


if __name__ == "__main__":
    main()

Script 2 | Revenue and Gross Margin (annual + quarterly)

Once I have the raw SEC company facts, I want to extract something useful. This script pulls annual revenue and gross profit, then calculates gross margin.

This script helps me answer:

  • How much revenue did the company generate each year?
  • How much gross profit was left after direct costs?
  • What was the company’s gross margin?
  • Are gross margins improving, stable, or getting worse?
  • Can I compare gross margins between similar companies?

Gross margin is one of the first numbers I like to check because it gives me a rough idea of business quality, pricing power, and cost structure. But I need to compare it carefully, a software company and a retailer should not be judged by the same margin expectations.

"""02_revenue_gross_profit.py --- Steps 2 & 3: revenue and gross margin."""
from edgar_utils import load_facts, flow_series, REVENUE_TAGS, margin, save_csv


def build(t):
    f = load_facts(t)
    rev_a, rev_q = flow_series(f, REVENUE_TAGS)
    gp_a, gp_q = flow_series(f, "GrossProfit")
    gm_a, gm_q = margin(gp_a, rev_a), margin(gp_q, rev_q)

    save_csv(f"{t}_annual_revenue_grossmargin.csv",
             ["fiscal_year_end", "revenue_usd", "gross_profit_usd", "gross_margin_pct"],
             [[d, rev_a[d], gp_a.get(d, ""), round(gm_a.get(d, float("nan")), 2)]
              for d in sorted(rev_a)])
    save_csv(f"{t}_quarterly_revenue_grossmargin.csv",
             ["quarter_end", "revenue_usd", "gross_profit_usd", "gross_margin_pct"],
             [[d, rev_q[d], gp_q.get(d, ""), round(gm_q.get(d, float("nan")), 2)]
              for d in sorted(rev_q)])
    return rev_a, rev_q, gm_a, gm_q


if __name__ == "__main__":
    for t in ["V", "MA"]:
        print(f"{t}:")
        build(t)

Script 3 | Compare Two Companies Visually

Tables are useful, but charts make patterns easier to see. This script takes the revenue and gross margin data from the previous step and turns it into simple comparison charts.

This script helps me answer questions like:

  • Which company has grown revenue faster?
  • Which company has more stable revenue?
  • Which company has higher gross margins?
  • Are margin differences consistent over time?
  • Is one company improving while another is weakening?

The chart does not make the investment decision for me. It simply shows me where to look more closely. If one company has much higher margins than another, the next question is not automatically “which one is better?”, the next question is: “why?”!

"""03_compare_visual.py --- Step 3: put two companies on the same chart."""
from edgar_utils import load_facts, flow_series, REVENUE_TAGS, margin, plot_compare

A, B = "V", "MA"


def rev_and_margin(t):
    f = load_facts(t)
    rev_a, rev_q = flow_series(f, REVENUE_TAGS)
    gp_a, gp_q = flow_series(f, "GrossProfit")
    return rev_a, rev_q, margin(gp_a, rev_a), margin(gp_q, rev_q)


if __name__ == "__main__":
    ra, rq, ma, mq = {}, {}, {}, {}
    for t in (A, B):
        ra[t], rq[t], ma[t], mq[t] = rev_and_margin(t)

    plot_compare(ra, f"{A} vs {B}: annual revenue", "Revenue ($B)",
                 f"{A}_{B}_annual_revenue.png", billions=True)
    plot_compare(ma, f"{A} vs {B}: annual gross margin", "Gross margin",
                 f"{A}_{B}_annual_grossmargin.png", pct=True)
    plot_compare(mq, f"{A} vs {B}: quarterly gross margin", "Gross margin",
                 f"{A}_{B}_quarterly_grossmargin.png", pct=True)

Script 4 | Compare Revenue Growth (year-over-year)

Revenue alone tells me the size of the business. Revenue growth tells me how fast the business is changing, this script calculates year-over-year revenue growth for each company.

This script helps me answer questions like:

  • Is the company growing or stagnating?
  • Is growth accelerating or slowing?
  • Is growth consistent or volatile?
  • Is one company growing faster than its competitors?
  • Did one year look unusually strong or unusually weak?

This is also where I need to be careful with seasonality. For many companies, quarter-over-quarter growth can be misleading. A retailer may always have a strong fourth quarter, a hardware company may move with product cycles, a cyclical business may look amazing at the top of a cycle, etc. That is why year-over-year comparison is often more useful.

"""04_revenue_growth.py --- Step 2: YoY revenue growth, annual & quarterly."""
from edgar_utils import load_facts, flow_series, REVENUE_TAGS, yoy_growth, save_csv, plot_compare

A, B = "KO", "PEP"


def growth(t):
    rev_a, rev_q = flow_series(load_facts(t), REVENUE_TAGS)
    return yoy_growth(rev_a), yoy_growth(rev_q)


if __name__ == "__main__":
    ga, gq = {}, {}
    for t in (A, B):
        ga[t], gq[t] = growth(t)
        save_csv(f"{t}_annual_revenue_yoy.csv", ["fiscal_year_end", "yoy_growth_pct"],
                 [[d, round(ga[t][d], 2)] for d in sorted(ga[t])])

    plot_compare(ga, f"{A} vs {B}: annual revenue growth (YoY)", "YoY growth",
                 f"{A}_{B}_annual_revenue_growth.png", pct=True)
    plot_compare(gq, f"{A} vs {B}: quarterly revenue growth (YoY)", "YoY growth",
                 f"{A}_{B}_quarterly_revenue_growth.png", pct=True)

Script 5 | Net Income and Net Margin

NOW, revenue growth is not enough. A company can grow revenue and still fail to produce attractive profits, this script extracts revenue and net income, then calculates net margin.

This script helps me answer:

  • How much profit is left after all expenses?
  • Is the company actually profitable?
  • Is net margin improving or deteriorating?
  • Is revenue growth translating into bottom-line profit?
  • Does one company keep more profit from each dollar of sales?

Net margin is useful, but I do not want to use it blindly. One-time gains, tax effects, restructuring costs, impairments, or unusual expenses can distort net income. If the number looks strange, I need to go back to the filing deep dive and understand why.

"""05_net_income_margin.py --- net income and net margin (annual + quarterly)."""
from edgar_utils import load_facts, flow_series, REVENUE_TAGS, margin, save_csv, plot_compare


def build(t):
    f = load_facts(t)
    rev_a, rev_q = flow_series(f, REVENUE_TAGS)
    ni_a, ni_q = flow_series(f, "NetIncomeLoss")
    nm_a, nm_q = margin(ni_a, rev_a), margin(ni_q, rev_q)
    save_csv(f"{t}_annual_net.csv",
             ["fiscal_year_end", "net_income_usd", "net_margin_pct"],
             [[d, ni_a[d], round(nm_a.get(d, float("nan")), 2)] for d in sorted(ni_a)])
    return nm_a, nm_q


if __name__ == "__main__":
    pair = ["AAPL", "MSFT"]
    data = {t: build(t)[0] for t in pair}
    plot_compare(data, f"{pair[0]} vs {pair[1]}: net margin (annual)", "Net margin",
                 "net_margin_annual.png", pct=True)

Script 6 | Operating Income and Operating Margin (Step 4)

Net income is useful, but operating income can sometimes give me a cleaner view of the core business. This script extracts revenue and operating income, then calculates operating margin.

This script helps me answer:

  • How profitable is the company’s core business?
  • Are operating expenses growing faster or slower than revenue?
  • Is the company showing operating leverage?
  • Are margins improving as the business scales?
  • Is the business becoming more efficient or more expensive to run?

Operating margin is especially useful when comparing companies in the same industry. If two similar businesses have very different operating margins, I want to understand whether the difference comes from scale, pricing power, cost discipline, business mix, or accounting differences.

"""06_operating_margin.py --- Step 4: operating income & margin (the missing piece)."""
from edgar_utils import load_facts, flow_series, REVENUE_TAGS, margin, save_csv, plot_compare


def build(t):
    f = load_facts(t)
    rev_a, rev_q = flow_series(f, REVENUE_TAGS)
    oi_a, oi_q = flow_series(f, "OperatingIncomeLoss")
    om_a, om_q = margin(oi_a, rev_a), margin(oi_q, rev_q)
    save_csv(f"{t}_annual_operating.csv",
             ["fiscal_year_end", "operating_income_usd", "operating_margin_pct"],
             [[d, oi_a[d], round(om_a.get(d, float("nan")), 2)] for d in sorted(oi_a)])
    return om_a, om_q


if __name__ == "__main__":
    pair = ["AAPL", "MSFT"]
    data = {t: build(t)[0] for t in pair}
    plot_compare(data, f"{pair[0]} vs {pair[1]}: operating margin (annual)",
                 "Operating margin", "operating_margin_annual.png", pct=True)

Script 7 | Operating Cash Flow, Capex, Free Cash Flow (Step 5)

Accounting profit matters, but cash matters too. This script extracts operating cash flow and capital expenditures, then calculates a simple version of free cash flow.

This script helps me answer:

  • Is the company generating cash from operations?
  • How much is being spent on capital expenditures?
  • Is free cash flow positive?
  • Is free cash flow growing or shrinking?
  • Is the company converting profit into real cash?

The simple formula used here is:

$$ Free cash flow = operating cash flow - capital expenditures $$

This is a useful starting point, but it is not perfect. Some companies require heavy reinvestment, some capital expenditures may support future growth (look at the CAPEX hyperscalers for AI) and some cash flow numbers may be affected by working capital timing. So I should not just look at free cash flow and stop thinking, iI should ask whether today’s spending is likely to create tomorrow’s value.

"""07_free_cash_flow.py --- Step 5: operating cash flow, capex, free cash flow.
FCF = operating_cash_flow - capex. (Capex tags report outflows as positives.)"""
from edgar_utils import load_facts, flow_series, save_csv, plot_compare

OCF_TAG = "NetCashProvidedByUsedInOperatingActivities"
CAPEX_TAGS = ["PaymentsToAcquirePropertyPlantAndEquipment",
              "PaymentsToAcquireProductiveAssets"]


def build(t):
    f = load_facts(t)
    ocf_a, ocf_q = flow_series(f, OCF_TAG)
    cap_a, cap_q = flow_series(f, CAPEX_TAGS)
    fcf_a = {d: ocf_a[d] - cap_a.get(d, 0) for d in ocf_a}
    fcf_q = {d: ocf_q[d] - cap_q.get(d, 0) for d in ocf_q}
    save_csv(f"{t}_annual_fcf.csv",
             ["fiscal_year_end", "operating_cash_flow_usd", "capex_usd", "free_cash_flow_usd"],
             [[d, ocf_a[d], cap_a.get(d, ""), fcf_a[d]] for d in sorted(ocf_a)])
    return fcf_a, fcf_q


if __name__ == "__main__":
    pair = ["AAPL", "MSFT"]
    data = {t: build(t)[0] for t in pair}
    plot_compare(data, f"{pair[0]} vs {pair[1]}: free cash flow (annual)",
                 "FCF ($B)", "fcf_annual.png", billions=True)

Script 8 | Cash vs. Debt (Step 6)

A company’s income statement tells me how the business performed. The balance sheet tells me how much financial strength or financial risk the company carries. This script extracts cash and debt-related values, then calculates a simple net cash estimate.

This script helps me answer:

  • How much cash does the company have?
  • How much debt does it carry?
  • Is debt increasing or decreasing?
  • Does the company have balance sheet flexibility?
  • Could debt become a problem if the business slows down?

This script is useful, but I need to be careful. Debt can appear in different forms: short-term debt, long-term debt, finance leases, notes payable, or other obligations, different companies may tag these items differently. So this script gives me a starting point, not a final verdict. If debt matters to the investment thesis, I need to check the original filing. The debt figure here is deliberately labeled an estimate. XBRL debt tagging is inconsistent enough that this is the perfect place to remember the garbage-in-garbage-out rule from Part 1: confirm against the balance sheet before trusting it.

"""08_cash_vs_debt.py --- Step 6: cash and debt (balance-sheet snapshots).

WARNING: debt tagging in XBRL is inconsistent across companies. The
'total_debt_est' below is a BEST EFFORT, always confirm against the
balance sheet in the actual 10-K / 10-Q!"""
from edgar_utils import load_facts, instant_series, save_csv, plot_compare

CASH_TAGS = ["CashAndCashEquivalentsAtCarryingValue"]
LONG_TERM_DEBT_TAGS = ["LongTermDebtNoncurrent", "LongTermDebt"]
CURRENT_DEBT_TAGS = ["LongTermDebtCurrent", "DebtCurrent"]


def build(t):
    f = load_facts(t)
    cash = instant_series(f, CASH_TAGS)
    lt = instant_series(f, LONG_TERM_DEBT_TAGS)
    cur = instant_series(f, CURRENT_DEBT_TAGS)
    debt = {d: lt.get(d, 0) + cur.get(d, 0) for d in (set(lt) | set(cur))}
    debt = dict(sorted(debt.items()))
    save_csv(f"{t}_cash_vs_debt.csv",
             ["period_end", "cash_usd", "total_debt_est_usd"],
             [[d, cash.get(d, ""), debt.get(d, "")]
              for d in sorted(set(cash) | set(debt))])
    return cash, debt


if __name__ == "__main__":
    for t in ["AAPL", "MSFT"]:
        cash, debt = build(t)
        plot_compare({f"{t} cash": cash, f"{t} debt (est.)": debt},
                     f"{t}: cash vs estimated total debt", "$B",
                     f"{t}_cash_vs_debt.png", billions=True)

Running the toolkit

Now, lets put this together shall we?

pip install requests matplotlib plotly
# edit USER_AGENT in edgar_utils.py to your real name + email
python 01_download_company_facts.py     # do this once
python 02_revenue_gross_profit.py       # then run any analysis script
python 06_operating_margin.py
# ...etc

CSVs and charts are saved in the edgar_output/ folder. Raw SEC downloads are cached in edgar_cache/. Open the CSVs and check the numbers against the original filings, that habit is the whole point, at least for now. Later, once I add LLM support, the workflow can become faster: the LLM can help summarize filings, compare sections, and flag numbers that look unusual. But I still do not want to blindly trust automation. If a number matters to the investment thesis, I want to verify it at the source (more on this later down the road).

Extra fun Script 9 | Combine the Data Into One Comparison Table

After pulling individual metrics, I want one cleaner table that brings the main numbers together. This script 09_valuation_dashboard.py combines revenue, margins, free cash flow, cash, and debt into one comparison file.

This script helps me answer:

  • Can I compare several companies in one table?
  • Which company has stronger margins?
  • Which company produces more free cash flow?
  • Which company has more balance sheet strength?
  • Which company deserves deeper research?

This is where the toolkit starts to feel useful, iInstead of jumping between separate files, I can create one summary table and use it as a research dashboard. But the dashboard is not the decision, it is the beginning of better questions.

One Important Warning About the Code

These scripts are not a professional accounting engine actually they are a learning toolkit which helps me pull structured data, compare companies, and find questions worth investigating. But SEC tags can vary, companies can report items differently, and some numbers require judgment. So when a number matters, I do not stop at this type of extraction, I go back to the filing and check it.

The Limitations of EDGAR

EDGAR is powerful, but it is not magic.

Filings are backward-looking. A 10-K tells me what happened; investing also requires thinking about what may happen next. Past growth does not guarantee future growth. EDGAR gives evidence, not certainty.

Companies use different tags. XBRL tags can differ between companies, which is why the scripts above try multiple candidate tags and why I still validate important numbers by hand. Automation is useful. LLMs can make the process faster. But blind automation is still dangerous.

Accounting is not always simple. Free cash flow, stock-based compensation, restructuring costs, one-time items, segment reporting, leases, deferred revenue, capitalized software, and goodwill impairments can all require judgment. As a beginner, I do not need to pretend I understand every detail. Improvement happens step by step.

Filings can be hard to read. They are legal, formal, and sometimes boring. But boring is not bad. Sometimes the dull documents hold the most important information. The goal is not to read everything perfectly; the goal is to keep improving.

How I Want to Use EDGAR Going Forward

My process is still developing, and I am not pretending to be a professional analyst. But I am getting better, one filing at a time. Before I buy a stock, I want to: read the latest 10-K and 10-Q, check recent 8-K filings, pull basic financial data with Python, compare the company against competitors, write down my thesis, write down what would prove me wrong, and avoid buying just because the stock is popular.

This is the real value of EDGAR for me: it slows me down. And sometimes slowing down is exactly what I need, because many investing mistakes happen when I rush.

Final Thoughts

EDGAR is not exciting at first. It does not give quick dopamine, it does not shout “buy now”, it does not promise easy money. But that is exactly why I like it: it forces me to deal with the business.

When I look at a company now, I am trying to read the signals that actually matter:

  • Revenue trend -> growing, or stagnating despite the hype?
  • Growth quality -> organic, or mostly acquisitions?
  • Margin direction -> improving, stable, or deteriorating?
  • Cash conversion -> is accounting profit turning into real cash?
  • Debt risk -> could debt become a problem if business slows?
  • Risk factors -> generic boilerplate, or something specific getting worse?
  • Management honesty -> clear explanations, or vague language hiding problems?
  • Valuation reality -> what expectations are already in the price?

For a beginner, this is one of the best habits to build. Not because EDGAR will make me right every time, it will not but because it helps me ask better questions. And better questions are the beginning of better investing.

I do not want to invest based only on noise, hype, or price movement. I want to understand what I own. EDGAR is one of the tools that helps me get there.

Not from expert to genius, but from zero to investor, step by step you build your way into investing.


Appendix

Disclaimer

This article is for educational purposes and reflects my own learning process. It is not investment advice. Always do your own research and maybe, if you feel the need to, consider speaking with a licensed professional before making investment decisions.

Share:

Comments

0
Most recent
No comments yet.