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.

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:
| Input | Where it comes from | Type |
|---|---|---|
| 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 statement | Filed inputs, derived result |
| Growth rate, years 1 to 10 | Your forecast, informed by historical financials | Assumption |
| Discount rate (WACC) | Risk-free rate, beta and equity risk premium, plus the cost of debt and tax rate from the 10-K | Market data and filed inputs |
| Terminal growth rate | Your forecast | Assumption |
| Cash, debt and shares | Balance sheet and the 10-K cover page | Filed |
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:
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')}")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 derivedlimit=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:
{
"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.
Show the numbers
| Fiscal year | Year ended | Operating cash flow | Capex | Free cash flow | Unlevered FCF | 10-K accession |
|---|---|---|---|---|---|---|
| FY2017 (53 wk) | 2017-09-03 | $6.726B | $2.502B | $4.224B | $4.272B | 0000909832-17-000014 |
| FY2018 | 2018-09-02 | $5.774B | $2.969B | $2.805B | $2.832B | 0000909832-18-000013 |
| FY2019 | 2019-09-01 | $6.356B | $2.998B | $3.358B | $3.336B | 0000909832-19-000019 |
| FY2020 | 2020-08-30 | $8.861B | $2.810B | $6.051B | $6.102B | 0000909832-20-000017 |
| FY2021 | 2021-08-29 | $8.958B | $3.588B | $5.370B | $5.391B | 0000909832-21-000014 |
| FY2022 | 2022-08-28 | $7.392B | $3.891B | $3.501B | $3.466B | 0000909832-22-000021 |
| FY2023 (53 wk) | 2023-09-03 | $11.068B | $4.323B | $6.745B | $6.468B | 0000909832-23-000042 |
| FY2024 | 2024-09-01 | $11.339B | $4.710B | $6.629B | $6.285B | 0000909832-24-000049 |
| FY2025 | 2025-08-31 | $13.335B | $5.498B | $7.837B | $7.511B | 0000909832-25-000101 |
| FY2026 | 2026-08-30 | $15.817B | $6.435B | $9.382B | $8.956B | 0000909832-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 flow | Built from | Discount at | What you get |
|---|---|---|---|
| Levered free cash flow | Operating cash flow minus capex, after interest paid (the API's freeCashFlow) | Neither: at the WACC, with debt subtracted later, the cost of debt counts twice | No clean value |
| Unlevered free cash flow (FCFF) | Levered free cash flow plus after-tax interest expense, minus after-tax non-operating income | WACC | Enterprise value |
| Free cash flow to equity (FCFE) | Levered free cash flow plus net borrowing | Cost of equity | Equity 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:InterestAndOtherIncomeand served asnonOperatingIncome. 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:
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")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.5BFCFF 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.
Show the numbers
| Year | Projected FCFF | Present 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:
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}")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.19Three 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
totalDebtof $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.
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}")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.58Costco'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:
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))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| Terminal g | 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 |
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:
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%}")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%Show the numbers
| Discount rate | Implied growth, FCFF | Implied 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.
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")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 aboveAll 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
| Trap | At Costco | Where the response shows it |
|---|---|---|
| A computed value presented as filed | Free 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 filings | The 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 values | The 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 calendars | Fiscal 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 switches | The 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 endpoints | Free plan | Paid plans |
|---|---|---|
1. Free cash flow | Latest fiscal year | Starter: 5 years. Stock, ETF, Professional: all 10 |
2. FCFF and terminal value | Latest fiscal year; no FCFF growth line | FCFF growth over 5 years (Starter) or 10 |
3. Value per share | Full step | Full step |
4. WACC | 2-year beta; the 5-year line reports the shorter history | Starter and up: both betas |
5. Sensitivity table None, local math | Full step | Full step |
6. Reverse DCF None, uses the step 4 quote | Full step | Full step |
7. SEC audit SEC companyconcept and submissions APIs | Full step | Full step |
Show the complete script, steps 1 to 7 in one file
# %% 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?
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?
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?
derived and computes it as operating cash flow minus capital expenditure.Q.What is the difference between levered and unlevered free cash flow?
Q.What discount rate should I use in a DCF?
Q.What is a reverse DCF?
Q.Should you subtract stock-based compensation from free cash flow in a DCF?
Q.Should operating leases count as debt in a DCF?
Q.Can I build this DCF on StockFit's free plan?
Ready to build?
Free API key, no credit card.