All Posts
TutorialDCF ModelPythonFree Cash Flow

How to Build a DCF Model in Python From 10-K Data

Build a DCF model in Python from 10-K data: free cash flow, WACC, terminal value, a sensitivity table and a reverse DCF, every input traced to the SEC.

Published October 23, 202619 min readStockFit Engineering
How to Build a DCF Model in Python From 10-K Data

The discounting in a DCF model takes a dozen lines of Python. The inputs are where a valuation goes wrong: a free cash flow figure copied from a table with no filing behind it, a balance sheet from a different period than the cash flows, a share count nobody checked. A typical Python DCF tutorial fetches a cash flow table from yfinance or a data vendor in one line and moves on, so nobody can say which filing a number came from or whether it was restated since.

This tutorial builds a discounted cash flow model in seven steps where every filed input carries the accession number of the SEC filing it came from, and the last step checks each one against SEC's own XBRL API for the same filing, period and unit. The worked example is Costco (COST), whose fiscal 2026 10-K was filed on October 7, 2026. You end with ten years of free cash flow, an unlevered free cash flow projection with a terminal value, the bridge from enterprise value to value per share, a WACC built from sourced market data, a sensitivity table, a reverse DCF, and an audit trail. You need Python 3.10 or later, the requests package, an API key in STOCKFIT_API_KEY, and your app name and contact email in SEC_USER_AGENT for the SEC requests in step 7. Every step runs on the Free plan with shorter history; the plan table at the end shows what changes. The growth, discount and terminal assumptions are illustrative, chosen to show the mechanics: the outputs are not price targets, and nothing here is investment advice.

What a DCF model needs, and which inputs are filed

A discounted cash flow model values a business as the present value of the cash it will generate. In practice that takes five inputs, and only some of them come from filings:

InputWhere it comes fromType
Base-year unlevered free cash flow (FCFF)Operating cash flow minus capital expenditure from the cash flow statement, plus after-tax interest expense, minus after-tax non-operating income from the income statementFiled inputs, derived result
Growth rate, years 1 to 10Your forecast, informed by historical financialsAssumption
Discount rate (WACC)Risk-free rate, beta and equity risk premium, plus the cost of debt and tax rate from the 10-KMarket data and filed inputs
Terminal growth rateYour forecastAssumption
Cash, debt and sharesBalance sheet and the 10-K cover pageFiled

The filed half should be exact and traceable. The judgment half should be explicit in code, where you can change it and watch what happens. Everything below follows that split. For the qualitative side of a valuation, the business, management and competition, start with our guide to fundamental stock analysis.

Step 1: pull free cash flow from the 10-K

Start with the cash flow statement. /api/financials/cash-flow-statement returns each fiscal year as a row of normalized facts, and includeSourceDetails=true adds the XBRL concept behind every value. The script fetches ten fiscal years and prints, for each, the accession number of the 10-K that supplied operating cash flow:

python
import os

import requests

BASE = "https://api.stockfit.io/v1/api"
HEADERS = {"Authorization": f"Bearer {os.environ['STOCKFIT_API_KEY']}"}
SYMBOL = "COST"


def get(path, **params):
    res = requests.get(f"{BASE}{path}", params=params, headers=HEADERS, timeout=30)
    if not res.ok:
        raise RuntimeError(f"{path} returned HTTP {res.status_code}: {res.text[:200]}")
    return res.json()


def filed_in(row, field):
    """Accession number of the filing that supplied `field`, or 'derived' if the API computed it."""
    for accession, source in row["sources"].items():
        if field in source["facts"]:
            return accession
    return "derived" if field in row["derived"] else None


cash_flows = get("/financials/cash-flow-statement", symbol=SYMBOL, period="annual", limit=10,
                 includeSourceDetails="true")

for row in reversed(cash_flows):
    f = row["facts"]
    print(f"FY{row['fiscalYear']}  OCF {f['operatingCashFlow'] / 1e9:5.2f}B  "
          f"capex {f['capitalExpenditure'] / 1e9:4.2f}B  FCF {f['freeCashFlow'] / 1e9:4.2f}B  "
          f"OCF from {filed_in(row, 'operatingCashFlow')}, FCF {filed_in(row, 'freeCashFlow')}")
output
FY2017  OCF  6.73B  capex 2.50B  FCF 4.22B  OCF from 0000909832-17-000014, FCF derived
FY2018  OCF  5.77B  capex 2.97B  FCF 2.81B  OCF from 0000909832-18-000013, FCF derived
FY2019  OCF  6.36B  capex 3.00B  FCF 3.36B  OCF from 0000909832-19-000019, FCF derived
FY2020  OCF  8.86B  capex 2.81B  FCF 6.05B  OCF from 0000909832-20-000017, FCF derived
FY2021  OCF  8.96B  capex 3.59B  FCF 5.37B  OCF from 0000909832-21-000014, FCF derived
FY2022  OCF  7.39B  capex 3.89B  FCF 3.50B  OCF from 0000909832-22-000021, FCF derived
FY2023  OCF 11.07B  capex 4.32B  FCF 6.75B  OCF from 0000909832-23-000042, FCF derived
FY2024  OCF 11.34B  capex 4.71B  FCF 6.63B  OCF from 0000909832-24-000049, FCF derived
FY2025  OCF 13.34B  capex 5.50B  FCF 7.84B  OCF from 0000909832-25-000101, FCF derived
FY2026  OCF 15.82B  capex 6.43B  FCF 9.38B  OCF from 0000909832-26-000093, FCF derived

limit=10 returns ten fiscal years on the Stock, ETF and Professional plans. Starter returns five and the Free plan the latest one, fiscal 2026 here, which is all the valuation in steps 2 to 7 needs. This is the fiscal 2026 row, trimmed to the fields the model reads:

json
{
  "period": "2026-08-30",
  "fiscalYear": 2026,
  "fiscalPeriod": "FY",
  "dateFiled": "2026-10-07",
  "acceptedAt": "2026-10-07T01:13:15Z",
  "facts": {
    "operatingCashFlow": 15817000000,
    "capitalExpenditure": 6435000000,
    "stockBasedCompensation": 924000000,
    "freeCashFlow": 9382000000
  },
  "sources": {
    "0000909832-26-000093": {
      "type": "10-K",
      "dateFiled": "2026-10-07",
      "acceptedAt": "2026-10-07T01:13:15Z",
      "amendment": false,
      "facts": {
        "operatingCashFlow": {
          "concept": "us-gaap:NetCashProvidedByUsedInOperatingActivities",
          "unit": "USD"
        },
        "capitalExpenditure": {
          "concept": "us-gaap:PaymentsToAcquirePropertyPlantAndEquipment",
          "unit": "USD"
        },
        "stockBasedCompensation": {
          "concept": "us-gaap:ShareBasedCompensation",
          "unit": "USD"
        }
      }
    }
  },
  "derived": [
    "issuanceOfDebt",
    "netIssuanceOfLongTermDebt",
    "netCommonStockIssuance",
    "netIssuancePaymentsOfDebt",
    "beginningCash",
    "changeInWorkingCapital",
    "changeInCash",
    "freeCashFlow"
  ]
}

Two details in that response matter for a DCF. First, freeCashFlow has no entry under the 10-K's accession. It sits in derived, because Costco does not report it: the phrase free cash flow does not appear in its fiscal 2026 10-K. The API computes it as operating cash flow minus capital expenditure, $15.817 billion minus $6.435 billion, which is $9.382 billion, and labels it as computed. Second, every filed value names its concept, so operating cash flow is us-gaap:NetCashProvidedByUsedInOperatingActivities from accession 0000909832-26-000093, not a vendor's reinterpretation of it. One more property matters in step 2: this free cash flow is levered, measured after the interest Costco paid.

Costco operating cash flow, split into capex and free cash flow, FY2017 to FY2026
Column height is operating cash flow as filed. Source: /api/financials/cash-flow-statement?symbol=COST&period=annual&limit=10, each year from its own 10-K, captured October 23, 2026. FY2017 and FY2023 ran 53 weeks. Hover a column for the accession number and the unlevered free cash flow (FCFF) that step 2 derives.
Show the numbers
Fiscal yearYear endedOperating cash flowCapexFree cash flowUnlevered FCF10-K accession
FY2017 (53 wk)2017-09-03$6.726B$2.502B$4.224B$4.272B0000909832-17-000014
FY20182018-09-02$5.774B$2.969B$2.805B$2.832B0000909832-18-000013
FY20192019-09-01$6.356B$2.998B$3.358B$3.336B0000909832-19-000019
FY20202020-08-30$8.861B$2.810B$6.051B$6.102B0000909832-20-000017
FY20212021-08-29$8.958B$3.588B$5.370B$5.391B0000909832-21-000014
FY20222022-08-28$7.392B$3.891B$3.501B$3.466B0000909832-22-000021
FY2023 (53 wk)2023-09-03$11.068B$4.323B$6.745B$6.468B0000909832-23-000042
FY20242024-09-01$11.339B$4.710B$6.629B$6.285B0000909832-24-000049
FY20252025-08-31$13.335B$5.498B$7.837B$7.511B0000909832-25-000101
FY20262026-08-30$15.817B$6.435B$9.382B$8.956B0000909832-26-000093

Free cash flow is lumpy. It fell to $3.501 billion in FY2022, the year merchandise inventories rose $4.003 billion, and reached $9.382 billion in FY2026. Growth rates computed from it depend on the endpoints: 9.3% a year from FY2017 to FY2026, 16.3% from FY2018, the low year, 11.8% over the last five years. Revenue grew more steadily, 10.0% a year from FY2017 to FY2026 and 9.1% over the last five, and free cash flow came to 3.1% of revenue in fiscal 2026. Those historical financials are context for the forecast, not the forecast.

Step 2: unlevered free cash flow and the terminal value

Under US GAAP, operating cash flow is measured after the interest a company pays and includes the interest it earns, so the free cash flow from step 1 is levered. A DCF that discounts at the weighted average cost of capital (WACC) values the whole firm, lenders included, and needs unlevered free cash flow, also called free cash flow to the firm (FCFF). Three definitions are worth keeping apart:

Cash flowBuilt fromDiscount atWhat you get
Levered free cash flowOperating cash flow minus capex, after interest paid (the API's freeCashFlow)Neither: at the WACC, with debt subtracted later, the cost of debt counts twiceNo clean value
Unlevered free cash flow (FCFF)Levered free cash flow plus after-tax interest expense, minus after-tax non-operating incomeWACCEnterprise value
Free cash flow to equity (FCFE)Levered free cash flow plus net borrowingCost of equityEquity value directly

This tutorial values the firm, so the code converts to FCFF with two lines from /api/financials/income-statement. Each adjustment is after tax at the fiscal 2026 effective rate of 24.7% ($3,025 million of tax on $12,251 million of pretax income), the same rate step 4 uses for the cost of debt:

  • Add back interest expense, $145 million, or $109 million after tax. Operating cash flow paid it, but interest belongs to lenders, whose claim step 3 subtracts as debt. The 10-K says interest expense is "primarily related to Senior Notes and financing leases": $64 million of it is interest on finance leases, which is why finance leases count as debt.
  • Subtract non-operating income, $711 million, or $535 million after tax: Interest income and other, net, filed as us-gaap:InterestAndOtherIncome and served as nonOperatingIncome. Step 3 adds the cash and investments that earn it at their balance sheet value, so leaving their return in FCFF as well would count them twice. The 10-K's MD&A splits the line into $601 million of interest income, $69 million of foreign-currency transaction gains and $41 million of other income. Costco reports all three below operating income and XBRL carries only the total, so the code removes the whole line. Subtracting only the interest income would put FCFF at $9.039 billion and the base case $6.19 a share higher.

Together the two adjustments take out, after tax, what Costco's segment note calls Other income: $566 million, interest expense netted against interest income and other. Both lines are filed values that step 7 checks, and the code applies them to every year in the history, each at its own effective tax rate:

python
incomes = {row["fiscalYear"]: row for row in get("/financials/income-statement", symbol=SYMBOL, period="annual",
                                                 limit=10, includeSourceDetails="true")}


def unlevered(cf_row):
    """FCFF and the tax rate behind it: free cash flow plus after-tax interest expense, minus after-tax
    non-operating income (the return on the cash that step 3 adds to the valuation separately)."""
    i = incomes[cf_row["fiscalYear"]]["facts"]
    tax_rate = i["incomeTaxExpense"] / i["incomeBeforeTax"]
    fcff = cf_row["facts"]["freeCashFlow"] + (i["interestExpense"] - i["nonOperatingIncome"]) * (1 - tax_rate)
    return fcff, tax_rate


base_year = cash_flows[0]["fiscalYear"]
income = incomes[base_year]
inc = income["facts"]
fcff0, tax_rate = unlevered(cash_flows[0])
bridge = [(f"FY{base_year} free cash flow (levered)", cash_flows[0]["facts"]["freeCashFlow"]),
          (f"+ interest expense x (1 - {tax_rate:.1%})", inc["interestExpense"] * (1 - tax_rate)),
          (f"- non-operating income x (1 - {tax_rate:.1%})", inc["nonOperatingIncome"] * (1 - tax_rate)),
          ("= unlevered free cash flow (FCFF)", fcff0)]
for label, value in bridge:
    print(f"{label:38}{value / 1e9:7.3f}B")

history = [(row["fiscalYear"], unlevered(row)[0]) for row in cash_flows if row["fiscalYear"] in incomes]
(last_year, last), (first_year, first) = history[0], history[-1]
if last_year > first_year and first > 0 and last > 0:
    print(f"FCFF growth, FY{first_year} to FY{last_year}: {(last / first) ** (1 / (last_year - first_year)) - 1:.1%} a year")

GROWTH = 0.09           # assumption: FCFF growth in years 1 to 10
DISCOUNT_RATE = 0.08    # placeholder, derived in step 4
TERMINAL_GROWTH = 0.03  # assumption: growth from year 11 on, forever
YEARS = 10


def dcf(fcf0, growth, discount_rate, terminal_growth, years=YEARS):
    """Enterprise value: PV of `years` of growing FCFF plus a perpetuity growth terminal value."""
    if discount_rate <= terminal_growth:
        raise ValueError("the discount rate must exceed terminal growth")
    fcf, pv_fcf = fcf0, []
    for t in range(1, years + 1):
        fcf *= 1 + growth
        pv_fcf.append(fcf / (1 + discount_rate) ** t)
    terminal_value = fcf * (1 + terminal_growth) / (discount_rate - terminal_growth)
    pv_terminal = terminal_value / (1 + discount_rate) ** years
    return sum(pv_fcf) + pv_terminal, pv_fcf, pv_terminal


ev, pv_fcf, pv_terminal = dcf(fcff0, GROWTH, DISCOUNT_RATE, TERMINAL_GROWTH)
print(f"PV of FCFF, years 1-{YEARS}: {sum(pv_fcf) / 1e9:.1f}B")
print(f"PV of terminal value: {pv_terminal / 1e9:.1f}B ({pv_terminal / ev:.0%} of enterprise value)")
print(f"Enterprise value: {ev / 1e9:.1f}B")
output
FY2026 free cash flow (levered)         9.382B
+ interest expense x (1 - 24.7%)        0.109B
- non-operating income x (1 - 24.7%)    0.535B
= unlevered free cash flow (FCFF)       8.956B
FCFF growth, FY2017 to FY2026: 8.6% a year
PV of FCFF, years 1-10: 94.2B
PV of terminal value: 202.3B (68% of enterprise value)
Enterprise value: 296.5B

FCFF of $8.956 billion is 4.5% below the levered figure, and it grew 8.6% a year from FY2017 to FY2026, slower than levered free cash flow, because non-operating income rose from $62 million to $711 million over those years. The projection period is ten years. FCFF grows at a constant rate, each year is discounted back at the discount rate, and everything after year 10 collapses into a terminal value with the perpetuity growth method (the Gordon growth formula): TV = FCF_10 * (1 + g) / (r - g). Three key assumptions drive it: 9% growth, close to the 8.6% FCFF growth and 10.0% revenue growth from FY2017 to FY2026; an 8% discount rate as a placeholder that step 4 derives; and 3% terminal growth.

Where the enterprise value comes from in the base case
FY2026 unlevered free cash flow (FCFF) of $8.956B grown 9% a year for ten years, discounted at 8%, then 3% terminal growth. Illustrative assumptions, output of the step 2 code run on October 23, 2026.
Show the numbers
YearProjected FCFFPresent value
1$9.76B$9.04B
2$10.64B$9.12B
3$11.60B$9.21B
4$12.64B$9.29B
5$13.78B$9.38B
6$15.02B$9.46B
7$16.37B$9.55B
8$17.84B$9.64B
9$19.45B$9.73B
10$21.20B$9.82B
Terminal value$436.8B$202.3B
Enterprise value$296.5B

The terminal value is 68% of the enterprise value: $202.3 billion of $296.5 billion. Ten years of explicit forecast contribute $94.2 billion. Because growth only slightly exceeds the discount rate, each year's present value barely rises, from $9.04 billion in year 1 to $9.82 billion in year 10, while the undiscounted cash flow more than doubles. That is why the terminal growth rate and the discount rate deserve more scrutiny than any single year of the forecast. The common alternative, the exit multiple method, sets the terminal value at a multiple of year-10 cash flow or EBITDA; it moves the same judgment into the multiple. One refinement the code leaves out: cash arrives through the year, not on its last day. The mid-year convention discounts year t at t - 0.5, which multiplies each discounted cash flow by (1 + r) ** 0.5, 3.9% more at 8%.

Step 3: from enterprise value to value per share

FCFF belongs to everyone who funds the company, so discounting it gives enterprise value. The equity value bridge adds the cash shareholders own, subtracts the debt owed ahead of them, and divides by the share count. Each item mirrors an adjustment in step 2, and all of it comes from /api/financials/balance-sheet, same 10-K. The code first checks that the balance sheet is from the same fiscal year as the cash flows:

python
balance = get("/financials/balance-sheet", symbol=SYMBOL, period="annual", limit=1,
              includeSourceDetails="true")[0]
if balance["fiscalYear"] != base_year:
    raise ValueError(f"balance sheet is FY{balance['fiscalYear']}, cash flows are FY{base_year}")
bal = balance["facts"]

cash = bal["cash"] + bal["shortTermInvestments"]  # their income left FCFF in step 2
debt = bal["longTermDebtCurrent"] + bal["longTermDebt"] + bal["financeLeaseLiability"]  # their interest went back in
shares = bal["currentSharesOutstanding"]  # from the 10-K cover page

equity_value = ev + cash - debt
print(f"Cash and short-term investments: {cash / 1e9:.3f}B")
print(f"Debt, including finance leases: {debt / 1e9:.3f}B")
print(f"Equity value: {equity_value / 1e9:.1f}B on {shares:,} shares")
print(f"Value per share: {equity_value / shares:,.2f}")
output
Cash and short-term investments: 21.301B
Debt, including finance leases: 7.575B
Equity value: 310.3B on 443,129,349 shares
Value per share: 700.19

Three choices in that block are worth defending:

  • Debt is borrowings plus finance leases: $2.248 billion of current and $3.914 billion of long-term debt ($6.162 billion of borrowings), plus $1.413 billion of finance lease liabilities, for $7.575 billion of total debt. These are the lenders whose interest step 2 added back. Operating lease liabilities ($2.606 billion) stay out: operating lease payments are operating costs that stay inside FCFF, so subtracting the liability as well would double count. Finance lease principal is paid in financing activities, outside FCFF, and the interest went back in step 2, so the liability belongs in debt. One finance lease item stays outside the model: assets Costco acquires under new finance leases, $122 million in fiscal 2026, never pass through capital expenditure. Counting them as investment would lower FCFF to $8.834 billion and the base case by $9.12 a share. The response also carries a derived totalDebt of $10.181 billion that includes both kinds of lease; building net debt from named fields shows exactly what is in it.
  • Shares are the cover-page count, 443,129,349 as of September 29, 2026 (dei:EntityCommonStockSharesOutstanding). Fiscal 2026 diluted weighted-average shares were 444,427,000; dividing by those instead lowers the value per share by 0.3%. For a company with a large option or RSU overhang, add the dilutive shares to the count.
  • Cash includes short-term investments: $20.207 billion of cash plus $1.094 billion of short-term investments. Step 2 took their income out of FCFF, so they enter the valuation once, at balance sheet value. The model treats all of it as surplus to operations; cash the business needs to run would belong in working capital instead, with its interest left in FCFF.

A complete bridge also subtracts preferred stock and noncontrolling interests, claims on the consolidated cash flows that common shareholders do not own. Costco has neither: preferredStock is 0 in the response, and its last reported noncontrolling interest was zero, at September 3, 2023. Under the step 2 assumptions the model puts the equity value at $310.3 billion, or $700.19 per share. That intrinsic value is only as good as the 8% discount rate behind it, which is the next step.

Step 4: calculate the discount rate (WACC) in Python

The discount rate is the weighted average cost of capital, WACC = E/(D+E) * cost_of_equity + D/(D+E) * cost_of_debt * (1 - tax_rate), with the cost of equity from CAPM, risk_free + beta * equity_risk_premium. StockFit does not serve a WACC or a beta. Both are estimates that depend on choices (the lookback window, the benchmark, the premium), so build them where those choices are visible, from sourced parts:

  • Risk-free rate: 5.00%, the 10-year Treasury par yield on October 23, 2026, from Treasury's daily par yield curve rates.
  • Equity risk premium: 4.20%, Aswath Damodaran's implied equity risk premium for October 1, 2026 (trailing 12 months, with adjusted payout), published on his NYU Stern site.
  • Beta, computed in the code from weekly returns of COST against SPY with /api/price/history, whose daily closes are split- and dividend-adjusted by default, so Costco's special dividends do not register as price drops. Weeks run Monday to Sunday, and the current, unfinished week is left out. Each week's price is the last close both symbols have that week, so the two returns always cover the same days, and a return only spans two consecutive weeks: a week missing from either series drops out instead of becoming a two-week return. The function refuses to estimate from fewer than 52 weekly returns, from a history that starts later than requested, or against a benchmark that never moves.
  • Tax rate and cost of debt from the income statement step 2 already fetched. The tax rate is the same 24.7%. The cost of debt is interest expense over the step 3 debt, 1.91%: finance lease interest sits in the numerator and finance leases in the denominator, so the two match. It is a backward-looking effective rate (a market yield on Costco's bonds would be higher), but debt is 1.7% of the capital structure, so it barely moves the result.
  • Market cap for the weights: the latest close from /api/price/quote times the cover-page share count.
python
import statistics
from datetime import date, datetime, timedelta, timezone

RISK_FREE = 0.0500  # 10-year Treasury par yield, 2026-10-23 (treasury.gov)
ERP = 0.0420        # implied equity risk premium, 2026-10-01 (Damodaran)
MIN_RETURNS = 52    # fewer weekly returns than this is too little history for a beta


def weekly_returns(symbol, benchmark, weeks):
    """Returns of both symbols over the same complete weeks (Monday to Sunday) before the current one. A week's
    price is the last daily close both symbols have in it, and a return only spans two consecutive weeks, so a
    missing week drops out instead of becoming a two-week return."""
    this_monday = date.today() - timedelta(days=date.today().weekday())
    start = this_monday - timedelta(weeks=weeks)
    closes = []
    for s in (symbol, benchmark):
        bars = get("/price/history", symbol=s, **{"from": start.isoformat()})["data"]  # adjusted daily closes
        closes.append({datetime.fromtimestamp(ts / 1000, tz=timezone.utc).date(): c for ts, c in bars})
    days = sorted(d for d in closes[0].keys() & closes[1].keys() if d < this_monday)
    if not days or (days[0] - start).days > 7:
        raise ValueError(f"price history starts {days[0] if days else 'nowhere'}, not {start} (plan limit?)")
    week_close = {d - timedelta(days=d.weekday()): d for d in days}  # Monday -> last common trading day
    mondays = sorted(week_close)
    pairs = [(week_close[a], week_close[b]) for a, b in zip(mondays, mondays[1:]) if b - a == timedelta(weeks=1)]
    return [[c[b] / c[a] - 1 for a, b in pairs] for c in closes]


def beta(symbol, benchmark, weeks):
    rs, rm = weekly_returns(symbol, benchmark, weeks)
    if len(rm) < MIN_RETURNS:
        raise ValueError(f"only {len(rm)} weekly returns, {MIN_RETURNS} needed")
    if statistics.variance(rm) == 0:
        raise ValueError(f"{benchmark} returns do not vary")
    return statistics.covariance(rs, rm) / statistics.variance(rm), len(rm)


cost_of_debt = inc["interestExpense"] / debt  # effective rate from the 10-K, backward-looking
quote = get("/price/quote", symbols=SYMBOL)[SYMBOL]
market_cap = quote["close"] * shares

print(f"Tax rate {tax_rate:.1%}, cost of debt {cost_of_debt:.2%}, debt weight {debt / (market_cap + debt):.1%}")
waccs = {}
for window, weeks in (("2-year", 104), ("5-year", 260)):
    try:
        b, n = beta(SYMBOL, "SPY", weeks)
    except ValueError as err:
        print(f"{window} beta skipped: {err}")
        continue
    cost_of_equity = RISK_FREE + b * ERP
    waccs[window] = (market_cap * cost_of_equity + debt * cost_of_debt * (1 - tax_rate)) / (market_cap + debt)
    value = (dcf(fcff0, GROWTH, waccs[window], TERMINAL_GROWTH)[0] + cash - debt) / shares
    print(f"{window} beta {b:.2f} ({n} weekly returns)  cost of equity {cost_of_equity:.2%}  "
          f"WACC {waccs[window]:.2%}  value per share {value:,.2f}")
output
Tax rate 24.7%, cost of debt 1.91%, debt weight 1.7%
2-year beta 0.33 (103 weekly returns)  cost of equity 6.39%  WACC 6.31%  value per share 1,072.26
5-year beta 0.74 (259 weekly returns)  cost of equity 8.11%  WACC 8.00%  value per share 700.58

Costco's beta is 0.33 measured over two years and 0.74 over five, from 103 and 259 weekly returns. Same stock, same API, two defensible lookback windows, and the WACC moves from 6.31% to 8.00%. Feed each into the model and the value per share moves between $700.58 and $1,072.26, 53% apart, with every filed input unchanged. The 8% placeholder survives as the five-year answer, but the honest output of this step is a range, and the next step makes it visible.

/api/price/history serves two years of history on the Free plan, five on Starter and the full history on Stock, ETF and Professional, and it trims a longer request without an error. The history check catches that: on the Free plan, the five-year line reports that the prices start two years back instead of printing a second two-year beta.

Step 5: a sensitivity table instead of one number

A DCF sensitivity analysis reruns the model across a grid of the assumptions that matter most, here the discount rate against the terminal growth rate:

python
RATES = [0.06, 0.07, 0.08, 0.09, 0.10]
TERMINAL_RATES = [0.02, 0.025, 0.03, 0.035, 0.04]

print("terminal g \\ discount " + "".join(f"{r:>8.0%}" for r in RATES))
for g in TERMINAL_RATES:
    values = [(dcf(fcff0, GROWTH, r, g)[0] + cash - debt) / shares for r in RATES]
    print(f"{g:>21.1%} " + "".join(f"{v:>8,.0f}" for v in values))
output
terminal g \ discount       6%      7%      8%      9%     10%
                 2.0%      949     751     620     528     458
                 2.5%    1,050     809     657     552     475
                 3.0%    1,185     881     700     580     495
                 3.5%    1,373     974     753     613     517
                 4.0%    1,657   1,098     820     653     543
DCF sensitivity table: value per share by discount rate and terminal growth
US$ per share, with unlevered free cash flow (FCFF) growing 9% a year for ten years. The outlined cell is the base case. Darker blue means a higher value. Output of the step 5 code, run on October 23, 2026. Illustrative assumptions, not a price target.
Terminal g6%7%8%9%10%
2.0%949751620528458
2.5%1,050809657552475
3.0%1,185881700580495
3.5%1,373974753613517
4.0%1,6571,098820653543
Columns: discount rate (WACC). Rows: terminal growth rate (g).

Across a 6% to 10% discount rate and 2% to 4% terminal growth, the value per share runs from $458 to $1,657. At 3% terminal growth, cutting the discount rate from 8% to 7% adds $181 a share; at an 8% discount rate, raising terminal growth from 3% to 4% adds $120. The corners are not equally plausible: as terminal growth approaches the discount rate the terminal value explodes, and the dcf() function refuses any terminal growth at or above it, where the formula breaks. Use the grid as a sanity check on how much of your answer is assumption.

Step 6: reverse DCF, the growth rate the price implies

A reverse DCF flips the question. Instead of asking what the cash flows are worth, it starts from the current share price and solves for the growth rate that makes the model match it. The code keeps terminal growth at 3% and searches for the years 1 to 10 FCFF growth rate by bisection, after checking that some rate between -50% and 100% can reproduce the price at all, which fails, for example, when base-year cash flow is negative:

python
def implied_growth(price, fcf0, discount_rate, terminal_growth, low=-0.5, high=1.0):
    """Years 1-10 FCFF growth at which the DCF value per share equals `price` (bisection)."""
    target_ev = price * shares - cash + debt

    def ev_at(growth):
        return dcf(fcf0, growth, discount_rate, terminal_growth)[0]

    if not ev_at(low) <= target_ev <= ev_at(high):
        raise ValueError(f"no growth rate from {low:.0%} to {high:.0%} values the stock at {price:,.2f}")
    for _ in range(100):
        mid = (low + high) / 2
        if ev_at(mid) < target_ev:
            low = mid
        else:
            high = mid
    return mid


sbc = cash_flows[0]["facts"]["stockBasedCompensation"]
print(f"Price {quote['close']:,.2f} on {quote['ts'][:10]}")
for r in (0.07, 0.08, 0.09):
    print(f"discount rate {r:.0%}: implied FCFF growth {implied_growth(quote['close'], fcff0, r, TERMINAL_GROWTH):.1%}"
          f", FCFF minus SBC {implied_growth(quote['close'], fcff0 - sbc, r, TERMINAL_GROWTH):.1%}")
output
Price 1,018.21 on 2026-10-22
discount rate 7%: implied FCFF growth 10.8%, FCFF minus SBC 12.2%
discount rate 8%: implied FCFF growth 14.0%, FCFF minus SBC 15.4%
discount rate 9%: implied FCFF growth 16.7%, FCFF minus SBC 18.2%
Reverse DCF: the FCFF growth Costco's share price implies
Growth for years 1 to 10 at which the model's value per share equals the October 22, 2026 close of $1,018.21, with 3% terminal growth. Vertical lines: the WACC from step 4 with each beta. Output of the step 6 code, run on October 23, 2026.
Show the numbers
Discount rateImplied growth, FCFFImplied growth, FCFF minus SBC
6.00%7.1%8.4%
6.25%8.1%9.5%
6.50%9.1%10.4%
6.75%10.0%11.3%
7.00%10.8%12.2%
7.25%11.7%13.0%
7.50%12.5%13.8%
7.75%13.2%14.6%
8.00%14.0%15.4%
8.25%14.7%16.1%
8.50%15.4%16.8%
8.75%16.1%17.5%
9.00%16.7%18.2%
9.25%17.4%18.8%
9.50%18.0%19.5%
9.75%18.6%20.1%
10.00%19.2%20.7%

At an 8% discount rate, Costco's October 22, 2026 close of $1,018.21 implies FCFF growing 14.0% a year for ten years, then 3% forever. From FY2017 to FY2026 it grew 8.6% a year. At the 6.31% WACC from the two-year beta, the same price implies 8.4%; at the 8.00% from the five-year beta, 14.0%. A reverse DCF does not say whether the price is right. It says what you have to believe to pay it, in a unit you can hold up against the historical financials.

Stock-based compensation raises the bar. Costco's fiscal 2026 stock-based compensation was $924 million, 10.3% of FCFF. Operating cash flow adds it back because no cash leaves the company, but employees are paid in shares instead. Subtract it from the base, as if it were cash pay, and the growth implied at 8% rises to 15.4%. That is a simplified approximation of the economic cost, not a model of dilution: it does not forecast the diluted share count, the shares Costco issues to employees, or the buybacks that offset some of them (Costco repurchased $848 million of its stock in fiscal 2026). A fuller model projects net share issuance and divides by a forecast share count instead; doing both would charge for the same shares twice. Our guide to stock-based compensation dilution shows how to measure what those shares cost. None of this is a view on whether Costco is a buy or a sell; it is the arithmetic of the assumptions.

Step 7: trace every DCF input to its SEC filing

The last step is the one DCF code usually skips: proving the inputs. For each filed value, the script reads the accession number, XBRL concept and unit from the response and asks SEC's companyconcept API for that concept and unit in that filing over the exact reporting period: the fiscal year from September 1, 2025 to August 30, 2026 for cash flow and income statement lines, its last day for balance sheet lines, and the cover-page date for the share count. The fiscal year comes from SEC's filing index: it ends on the 10-K's report date and starts the day after the previous 10-K's. Matching the period matters because one 10-K reports each line for several years, and some 10-Ks also tag fourth-quarter figures that end on the same day as the year. A fact SEC does not hold, or holds with two different values, is reported as unverified instead of being picked by position. SEC asks automated clients for a User-Agent naming the application and a contact email; the script reads it from SEC_USER_AGENT and stops if it is not set.

python
from functools import cache

SEC_HEADERS = {"User-Agent": os.environ["SEC_USER_AGENT"]}  # your app name and contact email


@cache
def sec_get(url):
    res = requests.get(url, headers=SEC_HEADERS, timeout=30)
    if res.status_code == 404:
        return None  # SEC has nothing at this URL, e.g. the company never filed the concept
    res.raise_for_status()
    return res.json()


def fiscal_year(cik, accession):
    """(start, end) of the fiscal year a 10-K covers, from SEC's filing index: it ends on the 10-K's report date
    and starts the day after the previous 10-K's."""
    recent = sec_get(f"https://data.sec.gov/submissions/CIK{cik:010d}.json")["filings"]["recent"]
    ends = sorted(d for form, d in zip(recent["form"], recent["reportDate"]) if form == "10-K")
    end = recent["reportDate"][recent["accessionNumber"].index(accession)]
    if ends.index(end) == 0:
        raise LookupError(f"no 10-K before {accession} to date the fiscal year from")
    return (date.fromisoformat(ends[ends.index(end) - 1]) + timedelta(days=1)).isoformat(), end


def sec_fact(cik, concept, unit, accession, start, end):
    """The one (start, end, value) SEC holds for a concept and unit in one filing: a duration (start and end), an
    instant (end only) or a cover-page instant (neither, the filer dates it). Raises on none or several."""
    prefix, name = concept.split(":")
    data = sec_get(f"https://data.sec.gov/api/xbrl/companyconcept/CIK{cik:010d}/{prefix}/{name}.json")
    found = {(x.get("start"), x["end"], x["val"]) for x in (data["units"].get(unit, []) if data else [])
             if x["accn"] == accession and x.get("start") == start and end in (None, x["end"])}
    if len(found) != 1:
        raise LookupError(f"{len(found)} SEC facts for {concept} [{unit}] {start or ''}..{end or 'cover'}")
    return found.pop()


tenk = filed_in(cash_flows[0], "operatingCashFlow")
fy_start, fy_end = fiscal_year(quote["cik"], tenk)
DURATION, INSTANT, COVER = (fy_start, fy_end), (None, fy_end), (None, None)
checks = [(cash_flows[0], "operatingCashFlow", DURATION), (cash_flows[0], "capitalExpenditure", DURATION),
          (cash_flows[0], "stockBasedCompensation", DURATION), (income, "interestExpense", DURATION),
          (income, "nonOperatingIncome", DURATION), (income, "incomeTaxExpense", DURATION),
          (income, "incomeBeforeTax", DURATION), (balance, "cash", INSTANT),
          (balance, "shortTermInvestments", INSTANT), (balance, "longTermDebtCurrent", INSTANT),
          (balance, "longTermDebt", INSTANT), (balance, "financeLeaseLiability", INSTANT),
          (balance, "currentSharesOutstanding", COVER)]

print(f"10-K {tenk}, fiscal year {fy_start} to {fy_end}")
for row, field, (start, end) in checks:
    accession = filed_in(row, field)
    if accession in (None, "derived"):
        print(f"{field:25} not a filed value")
        continue
    source = row["sources"][accession]["facts"][field]
    try:
        sec_start, sec_end, sec_value = sec_fact(quote["cik"], source["concept"], source["unit"], accession, start, end)
        status = "match" if row["facts"][field] == sec_value else f"SEC has {sec_value:,}"
        period = f"{sec_start} to {sec_end}" if sec_start else sec_end
    except LookupError as err:
        status, period = f"unverified: {err}", ""
    print(f"{field:25} {row['facts'][field]:>16,}  {accession}  {period:24}  {status}  {source['concept']}")
print(f"{'freeCashFlow':25} {cash_flows[0]['facts']['freeCashFlow']:>16,}  derived by the API: OCF - capex")
print(f"{'FCFF':25} {round(fcff0):>16,}  derived in step 2 from the filed values above")
output
10-K 0000909832-26-000093, fiscal year 2025-09-01 to 2026-08-30
operatingCashFlow           15,817,000,000  0000909832-26-000093  2025-09-01 to 2026-08-30  match  us-gaap:NetCashProvidedByUsedInOperatingActivities
capitalExpenditure           6,435,000,000  0000909832-26-000093  2025-09-01 to 2026-08-30  match  us-gaap:PaymentsToAcquirePropertyPlantAndEquipment
stockBasedCompensation         924,000,000  0000909832-26-000093  2025-09-01 to 2026-08-30  match  us-gaap:ShareBasedCompensation
interestExpense                145,000,000  0000909832-26-000093  2025-09-01 to 2026-08-30  match  us-gaap:InterestExpense
nonOperatingIncome             711,000,000  0000909832-26-000093  2025-09-01 to 2026-08-30  match  us-gaap:InterestAndOtherIncome
incomeTaxExpense             3,025,000,000  0000909832-26-000093  2025-09-01 to 2026-08-30  match  us-gaap:IncomeTaxExpenseBenefit
incomeBeforeTax             12,251,000,000  0000909832-26-000093  2025-09-01 to 2026-08-30  match  us-gaap:IncomeLossFromContinuingOperationsBeforeIncomeTaxesExtraordinaryItemsNoncontrollingInterest
cash                        20,207,000,000  0000909832-26-000093  2026-08-30                match  us-gaap:CashAndCashEquivalentsAtCarryingValue
shortTermInvestments         1,094,000,000  0000909832-26-000093  2026-08-30                match  us-gaap:ShortTermInvestments
longTermDebtCurrent          2,248,000,000  0000909832-26-000093  2026-08-30                match  us-gaap:LongTermDebtCurrent
longTermDebt                 3,914,000,000  0000909832-26-000093  2026-08-30                match  us-gaap:LongTermDebtNoncurrent
financeLeaseLiability        1,413,000,000  0000909832-26-000093  2026-08-30                match  us-gaap:FinanceLeaseLiability
currentSharesOutstanding       443,129,349  0000909832-26-000093  2026-09-29                match  dei:EntityCommonStockSharesOutstanding
freeCashFlow                 9,382,000,000  derived by the API: OCF - capex
FCFF                         8,955,755,938  derived in step 2 from the filed values above

All 13 filed inputs match SEC's own data under accession 0000909832-26-000093, Costco's fiscal 2026 10-K, for the same period and unit, and free cash flow and FCFF are labeled as derived. /api/filings/search-by-accession-number resolves the accession to the filing itself, with its filing date, period end and document URL, and SEC's EDGAR API documentation describes the endpoints the check uses.

What the receipts catch

TrapAt CostcoWhere the response shows it
A computed value presented as filedFree cash flow is not in the 10-K. It is operating cash flow minus capital expenditure.freeCashFlow is listed in derived
One fiscal year, two filingsThe FY2025 row takes 27 values from the FY2025 10-K and otherNonCashItems from the FY2026 10-K, which tagged it first.Two accessions under sources
Restated valuesThe FY2026 10-K revised FY2025 other current liabilities from $6.589 billion to $6.514 billion.before on the fact, next to its concept
Fiscal calendarsFiscal years ended between August 28 and September 3. FY2017 and FY2023 ran 53 weeks.period is a snapped key (every year reads 08-30): join on fiscalYear
Concept switchesThe FY2026 10-K tags revenue us-gaap:RevenueFromContractWithCustomerExcludingAssessedTax; the FY2025 10-K used us-gaap:Revenues.concept, with includeSourceDetails=true

Costco's fourth quarter runs 16 weeks (17 in a 53-week year) against 12 for the first three, the kind of calendar detail behind calculating Q4 earnings from a 10-K. And a DCF used in a backtest needs values as they were known on the date, not as revised later. The before field keeps the originally filed value, the starting point for point-in-time data for backtesting.

Run the complete DCF script

The seven cells above are one file, dcf_model.py. Set STOCKFIT_API_KEY and SEC_USER_AGENT and run it with python dcf_model.py; every output in this post was printed by that file on October 23, 2026. What each step needs from your plan:

Step and endpointsFree planPaid plans
1. Free cash flow
Latest fiscal yearStarter: 5 years. Stock, ETF, Professional: all 10
2. FCFF and terminal value
Latest fiscal year; no FCFF growth lineFCFF growth over 5 years (Starter) or 10
3. Value per share
Full stepFull step
4. WACC
2-year beta; the 5-year line reports the shorter historyStarter and up: both betas
5. Sensitivity table
None, local math
Full stepFull step
6. Reverse DCF
None, uses the step 4 quote
Full stepFull step
7. SEC audit
SEC companyconcept and submissions APIs
Full stepFull step
Show the complete script, steps 1 to 7 in one file
python
# %% Step 1: free cash flow from the 10-K
import os

import requests

BASE = "https://api.stockfit.io/v1/api"
HEADERS = {"Authorization": f"Bearer {os.environ['STOCKFIT_API_KEY']}"}
SYMBOL = "COST"


def get(path, **params):
    res = requests.get(f"{BASE}{path}", params=params, headers=HEADERS, timeout=30)
    if not res.ok:
        raise RuntimeError(f"{path} returned HTTP {res.status_code}: {res.text[:200]}")
    return res.json()


def filed_in(row, field):
    """Accession number of the filing that supplied `field`, or 'derived' if the API computed it."""
    for accession, source in row["sources"].items():
        if field in source["facts"]:
            return accession
    return "derived" if field in row["derived"] else None


cash_flows = get("/financials/cash-flow-statement", symbol=SYMBOL, period="annual", limit=10,
                 includeSourceDetails="true")

for row in reversed(cash_flows):
    f = row["facts"]
    print(f"FY{row['fiscalYear']}  OCF {f['operatingCashFlow'] / 1e9:5.2f}B  "
          f"capex {f['capitalExpenditure'] / 1e9:4.2f}B  FCF {f['freeCashFlow'] / 1e9:4.2f}B  "
          f"OCF from {filed_in(row, 'operatingCashFlow')}, FCF {filed_in(row, 'freeCashFlow')}")

# %% Step 2: unlevered free cash flow, projected, plus a terminal value
incomes = {row["fiscalYear"]: row for row in get("/financials/income-statement", symbol=SYMBOL, period="annual",
                                                 limit=10, includeSourceDetails="true")}


def unlevered(cf_row):
    """FCFF and the tax rate behind it: free cash flow plus after-tax interest expense, minus after-tax
    non-operating income (the return on the cash that step 3 adds to the valuation separately)."""
    i = incomes[cf_row["fiscalYear"]]["facts"]
    tax_rate = i["incomeTaxExpense"] / i["incomeBeforeTax"]
    fcff = cf_row["facts"]["freeCashFlow"] + (i["interestExpense"] - i["nonOperatingIncome"]) * (1 - tax_rate)
    return fcff, tax_rate


base_year = cash_flows[0]["fiscalYear"]
income = incomes[base_year]
inc = income["facts"]
fcff0, tax_rate = unlevered(cash_flows[0])
bridge = [(f"FY{base_year} free cash flow (levered)", cash_flows[0]["facts"]["freeCashFlow"]),
          (f"+ interest expense x (1 - {tax_rate:.1%})", inc["interestExpense"] * (1 - tax_rate)),
          (f"- non-operating income x (1 - {tax_rate:.1%})", inc["nonOperatingIncome"] * (1 - tax_rate)),
          ("= unlevered free cash flow (FCFF)", fcff0)]
for label, value in bridge:
    print(f"{label:38}{value / 1e9:7.3f}B")

history = [(row["fiscalYear"], unlevered(row)[0]) for row in cash_flows if row["fiscalYear"] in incomes]
(last_year, last), (first_year, first) = history[0], history[-1]
if last_year > first_year and first > 0 and last > 0:
    print(f"FCFF growth, FY{first_year} to FY{last_year}: {(last / first) ** (1 / (last_year - first_year)) - 1:.1%} a year")

GROWTH = 0.09           # assumption: FCFF growth in years 1 to 10
DISCOUNT_RATE = 0.08    # placeholder, derived in step 4
TERMINAL_GROWTH = 0.03  # assumption: growth from year 11 on, forever
YEARS = 10


def dcf(fcf0, growth, discount_rate, terminal_growth, years=YEARS):
    """Enterprise value: PV of `years` of growing FCFF plus a perpetuity growth terminal value."""
    if discount_rate <= terminal_growth:
        raise ValueError("the discount rate must exceed terminal growth")
    fcf, pv_fcf = fcf0, []
    for t in range(1, years + 1):
        fcf *= 1 + growth
        pv_fcf.append(fcf / (1 + discount_rate) ** t)
    terminal_value = fcf * (1 + terminal_growth) / (discount_rate - terminal_growth)
    pv_terminal = terminal_value / (1 + discount_rate) ** years
    return sum(pv_fcf) + pv_terminal, pv_fcf, pv_terminal


ev, pv_fcf, pv_terminal = dcf(fcff0, GROWTH, DISCOUNT_RATE, TERMINAL_GROWTH)
print(f"PV of FCFF, years 1-{YEARS}: {sum(pv_fcf) / 1e9:.1f}B")
print(f"PV of terminal value: {pv_terminal / 1e9:.1f}B ({pv_terminal / ev:.0%} of enterprise value)")
print(f"Enterprise value: {ev / 1e9:.1f}B")

# %% Step 3: from enterprise value to value per share
balance = get("/financials/balance-sheet", symbol=SYMBOL, period="annual", limit=1,
              includeSourceDetails="true")[0]
if balance["fiscalYear"] != base_year:
    raise ValueError(f"balance sheet is FY{balance['fiscalYear']}, cash flows are FY{base_year}")
bal = balance["facts"]

cash = bal["cash"] + bal["shortTermInvestments"]  # their income left FCFF in step 2
debt = bal["longTermDebtCurrent"] + bal["longTermDebt"] + bal["financeLeaseLiability"]  # their interest went back in
shares = bal["currentSharesOutstanding"]  # from the 10-K cover page

equity_value = ev + cash - debt
print(f"Cash and short-term investments: {cash / 1e9:.3f}B")
print(f"Debt, including finance leases: {debt / 1e9:.3f}B")
print(f"Equity value: {equity_value / 1e9:.1f}B on {shares:,} shares")
print(f"Value per share: {equity_value / shares:,.2f}")

# %% Step 4: the discount rate (WACC)
import statistics
from datetime import date, datetime, timedelta, timezone

RISK_FREE = 0.0500  # 10-year Treasury par yield, 2026-10-23 (treasury.gov)
ERP = 0.0420        # implied equity risk premium, 2026-10-01 (Damodaran)
MIN_RETURNS = 52    # fewer weekly returns than this is too little history for a beta


def weekly_returns(symbol, benchmark, weeks):
    """Returns of both symbols over the same complete weeks (Monday to Sunday) before the current one. A week's
    price is the last daily close both symbols have in it, and a return only spans two consecutive weeks, so a
    missing week drops out instead of becoming a two-week return."""
    this_monday = date.today() - timedelta(days=date.today().weekday())
    start = this_monday - timedelta(weeks=weeks)
    closes = []
    for s in (symbol, benchmark):
        bars = get("/price/history", symbol=s, **{"from": start.isoformat()})["data"]  # adjusted daily closes
        closes.append({datetime.fromtimestamp(ts / 1000, tz=timezone.utc).date(): c for ts, c in bars})
    days = sorted(d for d in closes[0].keys() & closes[1].keys() if d < this_monday)
    if not days or (days[0] - start).days > 7:
        raise ValueError(f"price history starts {days[0] if days else 'nowhere'}, not {start} (plan limit?)")
    week_close = {d - timedelta(days=d.weekday()): d for d in days}  # Monday -> last common trading day
    mondays = sorted(week_close)
    pairs = [(week_close[a], week_close[b]) for a, b in zip(mondays, mondays[1:]) if b - a == timedelta(weeks=1)]
    return [[c[b] / c[a] - 1 for a, b in pairs] for c in closes]


def beta(symbol, benchmark, weeks):
    rs, rm = weekly_returns(symbol, benchmark, weeks)
    if len(rm) < MIN_RETURNS:
        raise ValueError(f"only {len(rm)} weekly returns, {MIN_RETURNS} needed")
    if statistics.variance(rm) == 0:
        raise ValueError(f"{benchmark} returns do not vary")
    return statistics.covariance(rs, rm) / statistics.variance(rm), len(rm)


cost_of_debt = inc["interestExpense"] / debt  # effective rate from the 10-K, backward-looking
quote = get("/price/quote", symbols=SYMBOL)[SYMBOL]
market_cap = quote["close"] * shares

print(f"Tax rate {tax_rate:.1%}, cost of debt {cost_of_debt:.2%}, debt weight {debt / (market_cap + debt):.1%}")
waccs = {}
for window, weeks in (("2-year", 104), ("5-year", 260)):
    try:
        b, n = beta(SYMBOL, "SPY", weeks)
    except ValueError as err:
        print(f"{window} beta skipped: {err}")
        continue
    cost_of_equity = RISK_FREE + b * ERP
    waccs[window] = (market_cap * cost_of_equity + debt * cost_of_debt * (1 - tax_rate)) / (market_cap + debt)
    value = (dcf(fcff0, GROWTH, waccs[window], TERMINAL_GROWTH)[0] + cash - debt) / shares
    print(f"{window} beta {b:.2f} ({n} weekly returns)  cost of equity {cost_of_equity:.2%}  "
          f"WACC {waccs[window]:.2%}  value per share {value:,.2f}")

# %% Step 5: a sensitivity table instead of one number
RATES = [0.06, 0.07, 0.08, 0.09, 0.10]
TERMINAL_RATES = [0.02, 0.025, 0.03, 0.035, 0.04]

print("terminal g \\ discount " + "".join(f"{r:>8.0%}" for r in RATES))
for g in TERMINAL_RATES:
    values = [(dcf(fcff0, GROWTH, r, g)[0] + cash - debt) / shares for r in RATES]
    print(f"{g:>21.1%} " + "".join(f"{v:>8,.0f}" for v in values))

# %% Step 6: reverse DCF, the growth rate the price implies
def implied_growth(price, fcf0, discount_rate, terminal_growth, low=-0.5, high=1.0):
    """Years 1-10 FCFF growth at which the DCF value per share equals `price` (bisection)."""
    target_ev = price * shares - cash + debt

    def ev_at(growth):
        return dcf(fcf0, growth, discount_rate, terminal_growth)[0]

    if not ev_at(low) <= target_ev <= ev_at(high):
        raise ValueError(f"no growth rate from {low:.0%} to {high:.0%} values the stock at {price:,.2f}")
    for _ in range(100):
        mid = (low + high) / 2
        if ev_at(mid) < target_ev:
            low = mid
        else:
            high = mid
    return mid


sbc = cash_flows[0]["facts"]["stockBasedCompensation"]
print(f"Price {quote['close']:,.2f} on {quote['ts'][:10]}")
for r in (0.07, 0.08, 0.09):
    print(f"discount rate {r:.0%}: implied FCFF growth {implied_growth(quote['close'], fcff0, r, TERMINAL_GROWTH):.1%}"
          f", FCFF minus SBC {implied_growth(quote['close'], fcff0 - sbc, r, TERMINAL_GROWTH):.1%}")

# %% Step 7: trace every input to its filing
from functools import cache

SEC_HEADERS = {"User-Agent": os.environ["SEC_USER_AGENT"]}  # your app name and contact email


@cache
def sec_get(url):
    res = requests.get(url, headers=SEC_HEADERS, timeout=30)
    if res.status_code == 404:
        return None  # SEC has nothing at this URL, e.g. the company never filed the concept
    res.raise_for_status()
    return res.json()


def fiscal_year(cik, accession):
    """(start, end) of the fiscal year a 10-K covers, from SEC's filing index: it ends on the 10-K's report date
    and starts the day after the previous 10-K's."""
    recent = sec_get(f"https://data.sec.gov/submissions/CIK{cik:010d}.json")["filings"]["recent"]
    ends = sorted(d for form, d in zip(recent["form"], recent["reportDate"]) if form == "10-K")
    end = recent["reportDate"][recent["accessionNumber"].index(accession)]
    if ends.index(end) == 0:
        raise LookupError(f"no 10-K before {accession} to date the fiscal year from")
    return (date.fromisoformat(ends[ends.index(end) - 1]) + timedelta(days=1)).isoformat(), end


def sec_fact(cik, concept, unit, accession, start, end):
    """The one (start, end, value) SEC holds for a concept and unit in one filing: a duration (start and end), an
    instant (end only) or a cover-page instant (neither, the filer dates it). Raises on none or several."""
    prefix, name = concept.split(":")
    data = sec_get(f"https://data.sec.gov/api/xbrl/companyconcept/CIK{cik:010d}/{prefix}/{name}.json")
    found = {(x.get("start"), x["end"], x["val"]) for x in (data["units"].get(unit, []) if data else [])
             if x["accn"] == accession and x.get("start") == start and end in (None, x["end"])}
    if len(found) != 1:
        raise LookupError(f"{len(found)} SEC facts for {concept} [{unit}] {start or ''}..{end or 'cover'}")
    return found.pop()


tenk = filed_in(cash_flows[0], "operatingCashFlow")
fy_start, fy_end = fiscal_year(quote["cik"], tenk)
DURATION, INSTANT, COVER = (fy_start, fy_end), (None, fy_end), (None, None)
checks = [(cash_flows[0], "operatingCashFlow", DURATION), (cash_flows[0], "capitalExpenditure", DURATION),
          (cash_flows[0], "stockBasedCompensation", DURATION), (income, "interestExpense", DURATION),
          (income, "nonOperatingIncome", DURATION), (income, "incomeTaxExpense", DURATION),
          (income, "incomeBeforeTax", DURATION), (balance, "cash", INSTANT),
          (balance, "shortTermInvestments", INSTANT), (balance, "longTermDebtCurrent", INSTANT),
          (balance, "longTermDebt", INSTANT), (balance, "financeLeaseLiability", INSTANT),
          (balance, "currentSharesOutstanding", COVER)]

print(f"10-K {tenk}, fiscal year {fy_start} to {fy_end}")
for row, field, (start, end) in checks:
    accession = filed_in(row, field)
    if accession in (None, "derived"):
        print(f"{field:25} not a filed value")
        continue
    source = row["sources"][accession]["facts"][field]
    try:
        sec_start, sec_end, sec_value = sec_fact(quote["cik"], source["concept"], source["unit"], accession, start, end)
        status = "match" if row["facts"][field] == sec_value else f"SEC has {sec_value:,}"
        period = f"{sec_start} to {sec_end}" if sec_start else sec_end
    except LookupError as err:
        status, period = f"unverified: {err}", ""
    print(f"{field:25} {row['facts'][field]:>16,}  {accession}  {period:24}  {status}  {source['concept']}")
print(f"{'freeCashFlow':25} {cash_flows[0]['facts']['freeCashFlow']:>16,}  derived by the API: OCF - capex")
print(f"{'FCFF':25} {round(fcff0):>16,}  derived in step 2 from the filed values above")

To run it on another company, change SYMBOL and check four things the Costco run did not need: a negative or volatile base-year FCFF, where a single growth rate breaks down and a normalized base or a revenue and margin forecast works better; preferred stock and noncontrolling interests (minorityInterest), which step 3 should subtract when a company has them; what the company's non-operating line holds, since some filers put operating items or interest expense in it; and missing fields, which raise a KeyError naming the field instead of becoming a silent zero, so you decide whether absent means none.

If you build valuation tools, and run the script as it is, then explore the statement endpoints and their source details, starting with /api/financials/cash-flow-statement and includeSourceDetails=true. Ten years of statements and the full price history are on the Stock, ETF and Professional plans, compared on the pricing page, and for valuation models across many companies the same statements power our point-in-time fundamentals API for quants.

FAQ

Q.How do you build a DCF model in Python?
Pull operating cash flow and capital expenditure from the cash flow statement and convert the resulting free cash flow to unlevered free cash flow (FCFF) by adding back after-tax interest expense and removing after-tax non-operating income. Project FCFF over a projection period at an assumed growth rate, add a terminal value with the perpetuity growth formula, and discount everything at the WACC. Then bridge from enterprise value to equity value by adding cash, subtracting debt and dividing by shares outstanding. The script in this tutorial does it in seven steps with the requests library and checks every filed input against SEC's XBRL API.
Q.Where can I get free cash flow data for a DCF model?
From the cash flow statement in the company's 10-K: operating cash flow minus capital expenditure. /api/financials/cash-flow-statement returns both lines with the accession number of the filing they came from, plus a derived freeCashFlow field, and /api/financials/income-statement has the interest expense and non-operating income that turn it into unlevered free cash flow. SEC's own XBRL API at data.sec.gov has the raw facts, one concept per request.
Q.Is free cash flow reported in the 10-K?
Usually not as a line item. Free cash flow is a non-GAAP measure, and companies that use it define it themselves. Costco's fiscal 2026 10-K does not use the phrase at all, so any free cash flow figure for Costco is computed. StockFit lists it under derived and computes it as operating cash flow minus capital expenditure.
Q.What is the difference between levered and unlevered free cash flow?
Levered free cash flow is operating cash flow minus capital expenditure, measured after the interest a company pays. Unlevered free cash flow, or free cash flow to the firm (FCFF), adds back after-tax interest expense and removes after-tax non-operating income, so it is the cash available to lenders and shareholders together; discounted at the WACC, it gives enterprise value. Free cash flow to equity adds net borrowing to levered free cash flow and is discounted at the cost of equity. For Costco's fiscal 2026, levered free cash flow was $9.382 billion and FCFF $8.956 billion.
Q.What discount rate should I use in a DCF?
The weighted average cost of capital: the cost of equity (risk-free rate plus beta times the equity risk premium) and the after-tax cost of debt, weighted by market values. For Costco, a 5.00% risk-free rate on October 23, 2026, a 4.20% equity risk premium and a five-year beta of 0.74 give 8.00%; the two-year beta of 0.33 gives 6.31%. Because the choice moves the valuation so much, show a range in a sensitivity table rather than one rate.
Q.What is a reverse DCF?
A reverse DCF starts from the current share price and solves for the growth rate that makes the DCF value equal it. At an 8% discount rate and 3% terminal growth, Costco's October 22, 2026 close of $1,018.21 implies unlevered free cash flow growth of 14.0% a year for ten years, against 8.6% a year from fiscal 2017 to fiscal 2026.
Q.Should you subtract stock-based compensation from free cash flow in a DCF?
It is a reasonable approximation if the valuation should reflect what shareholders bear. Stock-based compensation is added back in operating cash flow because no cash leaves, but it is paid in shares, which dilutes existing owners. Subtracting it treats it as a cash cost; it does not forecast the share count, new issuance or the buybacks that offset it, and a model that projects dilution explicitly should not subtract it as well. Costco's fiscal 2026 stock-based compensation was $924 million, 10.3% of unlevered free cash flow, and subtracting it raises the growth implied by the October 22, 2026 price from 14.0% to 15.4% at an 8% discount rate.
Q.Should operating leases count as debt in a DCF?
Not when the model starts from operating cash flow. Operating lease payments are already deducted inside operating cash flow, so subtracting the lease liability as well double counts. Finance leases are different: their interest is in interest expense, which unlevered free cash flow adds back, and their principal is paid in financing activities, so finance lease liabilities belong in debt. On Costco's fiscal 2026 balance sheet that means $1.413 billion of finance leases in and $2.606 billion of operating leases out.
Q.Can I build this DCF on StockFit's free plan?
Yes. Every step runs on the Free plan, which returns the latest fiscal year of each statement, two years of daily prices and one quote per request. The longer views need a paid plan: the ten-year history in steps 1 and 2 needs the Stock, ETF or Professional plan (Starter returns five years), and the five-year beta needs Starter or above; on the Free plan the script reports the shorter price history instead of printing a wrong beta.

Ready to build?

Free API key, no credit card.