Cost of capital toolkit

SkillCommerce & finance

Estimates discount rates and the optimal financing mix — riskfree rate in any currency, equity risk premium including country risk, bottom-up and total betas, synthetic credit ratings from interest coverage, cost of debt, market value of debt, WACC, the implied equity risk premium solved from current index levels, and the cost-of-capital schedule across debt ratios that locates the optimal debt ratio, with an APV cross-check and a stress test under rating constraints. Bundles Damodaran's rating and spread tables, Altman default probabilities, industry-average betas and multiples, and country risk premiums, each tagged with its vintage. Use when estimating a discount rate, WACC, hurdle rate, cost of equity or cost of debt, unlevering or relevering a beta, when a company has no credit rating, when estimating the equity risk premium priced into the market, when looking up a sector beta or country risk premium, when converting a rate between currencies, or when finding or stressing an optimal capital structure.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Cost of capital toolkit skill

What this skill tells your AI

The instructions your AI receives, as published by lyndonkl/claude in skills/cost-of-capital-toolkit/SKILL.md and read by ahel’s review.

Every discounted valuation needs a rate. Most valuation errors that survive review are rate errors. A rate built in one currency gets applied to cash flows in another. A regression beta describes a period that no longer matches the business. Book weights sit where market weights belong.

The arithmetic is fully scripted. Your job is choosing the inputs — which comparables, which premium estimator, gross or net debt — and defending those choices.

The scripts

Two of them, both pure standard library with nothing to install. Every subcommand takes JSON on stdin (or --in FILE) and prints JSON.

resources/costofcapital.py does the arithmetic.

python3 resources/costofcapital.py <subcommand> --example    # show input shape
python3 resources/costofcapital.py <subcommand> --in payload.json
python3 resources/costofcapital.py selftest                  # verify the engine
SubcommandTurns thisInto this
ratingEBIT and interest expensesynthetic rating, default spread, pre-tax and after-tax cost of debt
betacomparable betas, or one levered betaunlevered, relevered, bottom-up, or total beta
mv-debtbook debt, interest expense, maturitymarket value of debt
wacccomponent costs and valuesWACC with weights
debt-scheduleunlevered beta, EBIT, firm valuecost of capital at every debt ratio, the optimum, and the value of moving there
implied-erpindex level and expected cash flowsthe expected return and equity risk premium the market is pricing in
apvequity, debt, tax rate, bankruptcy costunlevered value, tax shield and expected distress cost at every debt ratio
stressa debt-schedule payload plus haircuts or a rating floorhow far the optimum moves under weaker EBIT, and what a rating floor costs
convert-ratea rate and two inflation ratesthe same rate in another currency

resources/reference_data.py serves the bundled tables. Every other engine reads its industry and country numbers through here, so a valuation cannot quietly mix a beta from one year with a country premium from another.

python3 resources/reference_data.py <subcommand> --example
python3 resources/reference_data.py <subcommand> --in payload.json
python3 resources/reference_data.py selftest
SubcommandTurns thisInto this
lookupan industry, country or region namethe full reference row, tagged with its vintage
erp-for-operationsrevenue shares by countrythe revenue-weighted equity risk premium, and the cost of equity if you add a beta
vintagea valuation datethe as_of of every bundled table, and a warning for each one the date has outrun

Run selftest on both after editing anything. Between them they check the engines against worked examples from the source models, including a full cost-of-capital build that reproduces its sheet to eight decimals.

Build the rate in this order

Each step feeds the next. Do not skip ahead: a beta is meaningless until you know the currency, and weights are meaningless until debt is at market value.

Cost of capital progress:
- [ ] 1. Fix the currency, and the riskfree rate in it
- [ ] 2. Build the equity risk premium, including country risk
- [ ] 3. Estimate the beta bottom-up
- [ ] 4. Estimate the cost of debt
- [ ] 5. Weight at market values and assemble
- [ ] 6. Check consistency

1. Currency and riskfree rate

The currency of the rate must match the currency of the cash flows. That is a hard constraint, not a preference — mismatching them builds an inflation differential into the answer that has nothing to do with the business.

Take the long-term government bond yield when the government is default-free. When it is not, strip the sovereign default spread out of the local bond yield:

riskfree rate = local government bond yield − sovereign default spread

Get the sovereign spread from a rating-based table, a sovereign CDS, or the gap between the government's local and hard-currency bonds. If no local bond exists, build the rate from a mature-market rate and the inflation differential instead. Details and the negative-rate case: resources/methodology.md.

Resist the urge to "normalize" a low riskfree rate upward. Doing so while leaving the equity risk premium alone quietly raises every discount rate and understates every company. If the rate looks wrong, change the cash flows, not the anchor.

2. Equity risk premium

Two layers. Start with a mature-market premium, then add country risk for where the company actually operates, not where it is incorporated.

For the mature-market layer, compute the implied premium rather than reaching for a historical average. Take today's index level as the price, forecast the cash the index will return to its investors, and solve for the rate that makes the two agree. That rate is the expected return on stocks; subtract the riskfree rate and the rest is the premium:

python3 resources/costofcapital.py implied-erp --in index.json

Supply the cash flows one of three ways: an explicit cash_flows list, an earnings path with payout_ratios, or a base_cash_flow (or dividend_yield) with a growth_rate. Whichever you use, the cash flow is dividends plus buybacks, not dividends alone. Buybacks were 68.89 of the S&P 500's 127.78 in 2020; leaving them out roughly halves the answer. Terminal growth defaults to the riskfree rate and is capped there, because no perpetual growth rate can outrun the economy it grows in.

Why bother: the historical average is a mean with a standard error of roughly two points on a five-point estimate, which is wide enough to swallow the number whole. The implied premium has no standard error at all, and it moves — 4.83% in February 2020, 7.75% at the March bottom, 5.35% by November. Re-run it whenever the market moves and record the date you used. The same procedure works on any index: feed it the local level, the local cash yield and the local riskfree rate.

Then the country layer:

company ERP = mature market ERP + Σ (revenue share by country × country risk premium)

A country risk premium is the sovereign default spread scaled up for equity's greater volatility. Weighting by operations matters: a Brazilian company earning most of its revenue in the United States does not carry Brazil's full risk, and a US-incorporated company earning most of its revenue in Brazil does.

echo '{"operations": [{"country": "Brazil", "revenue": 130},
                      {"country": "Canada", "revenue": 23}]}' \
  | python3 resources/reference_data.py erp-for-operations

Rows name a country or a region. Where a filer reports a bucket that maps to nothing clean — "rest of world", "Eurasia", "Pacific" — give that row its own erp and say in the writeup where the number came from. Weight by whatever measure locates the risk: revenue usually, production for a resource company, assets for a manufacturer. Add a riskfree_rate and a beta and the answer carries a cost of equity under both attachments, adding the country premium and scaling it by beta, which on real emerging market cases can differ by several points.

3. Beta

Use a bottom-up beta. Regression betas carry standard errors so large the point estimate is rarely usable, and they describe a past business mix rather than the current one.

python3 resources/costofcapital.py beta --in businesses.json

Take the median unlevered beta of comparable firms in each business the company operates in, weight by each business's estimated value, then relever at the company's own debt-to-equity ratio:

levered beta = unlevered beta × (1 + (1 − tax rate) × debt/equity)

Weight by value, and when segment values are not disclosed, approximate them as segment revenue times a sector EV/Sales multiple. Both numbers come out of the same lookup:

echo '{"industry": "Aerospace/Defense",
       "fields": ["unlevered_beta", "ev_sales"]}' \
  | python3 resources/reference_data.py lookup

Names must match the table, but case and stray whitespace do not matter, and a miss comes back with the near matches rather than nothing. Watch for labels the source sheet truncated mid-word — Insurance (Prop/Cas.) is the real key, not a typo.

Use a total betaoperation: total — when valuing a company for an owner who cannot diversify, typically a private business. It scales the market beta up by the inverse of the correlation with the market, charging the owner for the firm-specific risk a diversified investor would shed.

4. Cost of debt

What matters is what the company would pay to borrow today, not the coupon on debt it issued years ago and not the rate a parent guarantees.

Prefer, in order: the yield to maturity on the company's own traded long-term bonds; the spread implied by its agency rating; a synthetic rating from interest coverage.

echo '{"ebit": 2000, "interest_expense": 250, "riskfree_rate": 0.04,
       "marginal_tax_rate": 0.25, "table": "large_manufacturing"}' \
  | python3 resources/costofcapital.py rating

Pick the table that matches the company: large_manufacturing for large non-financial firms, small_or_risky for smaller or more volatile ones (the same coverage buys a worse rating), financial_service for banks and insurers.

pre-tax cost of debt = riskfree rate + company default spread + country default spread

Only the marginal tax rate matters for the after-tax cost, and only to the extent there is income to shelter — the engine caps the benefit when interest exceeds EBIT.

Capitalized leases create a circularity. Discounting lease commitments needs a cost of debt; a synthetic rating needs interest coverage, which lease interest changes. Iterate: compute a rate, capitalize leases, recompute coverage, recompute the rate, until it stops moving. debt-schedule already does this internally.

5. Weights and assembly

Weight at market values. Book weights systematically understate equity for a profitable company. Convert book debt to market by treating all of it as one coupon bond:

python3 resources/costofcapital.py mv-debt --in debt.json

Use the weighted-average maturity of outstanding debt; where it is not disclosed, three years is the working default. Add capitalized lease debt afterwards — it is already a present value.

WACC = cost of equity × E/(D+E+P)
     + pre-tax cost of debt × (1 − tax rate) × D/(D+E+P)
     + cost of preferred × P/(D+E+P)

Preferred stock is a third component when it is material; below about 5% of capital, fold it into debt rather than carrying a third term.

6. Consistency checks

  • Currency of the rate equals currency of the cash flows.
  • Weights sum to one and use market values.
  • The tax rate used to relever the beta is the same one used for the after-tax cost of debt.
  • Gross debt throughout, or net debt throughout — never mixed.
  • The levered beta reflects the company's current debt ratio, not the comparables'.

Finding the optimal debt ratio

The same machinery run in a loop. At each candidate debt ratio the engine relevers the beta, re-prices the debt through a fresh synthetic rating, caps the tax benefit at available income, and computes the cost of capital. The ratio with the lowest cost of capital is the optimum, because the same cash flows discounted at a lower rate are worth more.

python3 resources/costofcapital.py debt-schedule --in firm.json

Read the result with judgment:

  • The curve is usually flat near the bottom. A move from 30% to 40% debt that saves four basis points is not an argument for anything. Look at the shape, not just the minimum.
  • Banks are excluded. For a financial service firm, regulatory capital sets the financing mix. This calculation does not apply.

Stress the answer, or constrain it

The optimum falls out of one number held fixed: operating income. Real operating income moves. stress re-runs the same schedule with EBIT knocked down and reports where the answer goes.

python3 resources/costofcapital.py stress --in firm.json

Take a debt-schedule payload and add whichever of these you have:

InputEffect
haircutsre-runs at EBIT × (1 − h) for each h; defaults to 10% steps down to 60%
recession_ebitre-runs at a stated recession level instead of a percentage cut
ebit_historycomputes the standard deviation of annual percentage changes, then stresses by n_sigma of them (default 3)
required_ratingfinds the highest debt ratio that still earns that rating, and prices the constraint

The output's safety_buffer is the largest decline the optimum survives. Read it against the firm's own record. Disney's 40% optimum held through a 30% cut. That is deeper than its worst year on file and well past one standard deviation. A firm with that much room is already protected; buying a rating floor on top pays twice for the same thing.

A required_rating is priced two ways: cost_of_capital_given_up, which is always available, and value_given_up, which needs a cash flow and a growth rate to fall out of the base schedule. Quote the second one to a board. "Sixteen basis points" does not land the way a dollar figure does.

Use one protection, not both. A rating floor already guards against weak operating income. Stacking a haircut on top leaves the firm under-levered for reasons nobody wrote down. And separate the honest reasons for a rating floor from the vain one: protection against downside risk is real, a ratings drop that itself damages the business is real, and wanting to say the company is AAA-rated is not.

The APV cross-check

apv answers the same question by different machinery, which is what makes it worth running. Rather than folding distress risk into a default spread, it splits value into three pieces: the firm with no debt, plus the tax shield debt creates, minus the expected cost of the bankruptcy debt makes more likely.

python3 resources/costofcapital.py apv --in firm.json
firm value = unlevered value + tax benefit(d) − p(default at d) × bankruptcy cost % × value at risk

Unlevered value is backed out of today's market price by removing the tax benefit the market already pays for and adding back the distress cost it already charges for. Supply ebit and riskfree_rate to have the rating at each debt ratio solved from interest coverage, or supply a ratings list with one entry per debt ratio when you are reproducing a published schedule or working from a rating outlook.

Two soft numbers drive the answer: bankruptcy_cost_pct and the default probability. Direct bankruptcy costs run 5-10% of firm value; indirect costs are a judgment about how badly customers, suppliers and employees would punish visible distress, and Damodaran's worked examples use 25% in total. Treat the optimum as a region.

The two approaches should land in the same neighbourhood. When they diverge sharply, suspect the bankruptcy cost assumption or the default probability table before you suspect the arithmetic. The bundled probability column is genuinely non-monotonic, and that alone can put the peak at a higher debt ratio than the economics would justify.

See resources/optimal-capital-structure.md for the enhanced approach with indirect bankruptcy costs and the mechanics of actually moving to the optimal ratio.

Reference data

The tables ship in resources/data/, each tagged with an as_of date and the source sheet it was lifted from. The four the rate build leans on:

FileHolds
synthetic_ratings.jsoninterest-coverage bands by firm type, and the rating-to-spread map
industry_averages.json96 rows of US industry averages — betas, costs of capital, sales-to-capital, multiples, margins, reinvestment rates
country_risk.json186 sovereigns and 10 regional aggregates — rating, default spread, country risk premium, total ERP, corporate tax rate
default_probabilities.jsonAltman's rating-to-default-probability map, used by apv

default_probabilities.json carries cumulative ten-year default rates by original rating. Read down it and the column is not monotonic: Ba1/BB+ at 0.10 sits below Ba2/BB at 0.1663. That is what the studies report, and it is reproduced verbatim, because it is what makes an APV schedule peak where it does. Do not tidy it. Override it with probabilities_path, and the ratings tables with ratings_path.

Start any valuation by checking how old they are against the date you are valuing as of. The check reads whatever is in the directory, so a table added later is covered too:

echo '{"valuation_date": "2024-06-30"}' | python3 resources/reference_data.py vintage

Anything more than a year past its as_of gets a warning naming where to refresh it. Damodaran reposts all three every January, and spreads in particular move by a factor of three across vintages, so a stale table is not a rounding problem. Refresh into a copy and point at it, rather than editing the bundled files.

Two habits worth keeping:

  • Never mix vintages inside one valuation. A 2020 beta with a 2022 country premium is a number nobody can reproduce. Pass vintage on a lookup and it refuses to answer from a table that does not match.
  • null is not zero. Margins, sales-to-capital, EV/EBITDA and working-capital columns come back null for banks, brokers and insurers, because the ratio is not defined for them. Read a null as zero and every average over that column is wrong.

Common failures

SymptomCause
Emerging-market company looks permanently cheapCountry risk applied by incorporation instead of operations
Beta swings wildly between updatesRegression beta instead of bottom-up
WACC below the cost of debtBook weights, or a tax benefit not capped at available income
Value changes when the currency changesRate and cash flows in different currencies
Rating implausible for an obviously healthy firmWrong table, or EBIT not adjusted for leases and research spending first
Optimal debt ratio at an implausible 80%EBIT taken at a cyclical peak, or no rating constraint applied
Sector benchmarks nobody can reproduceTwo reference tables of different vintages in the same build
A bank's sector multiple comes out at zeroA null read as a zero — the ratio is undefined for banks, not nil
Implied ERP comes out near half the published figureDividends used as the index cash flow, with buybacks left out
Implied ERP looks suspiciously lowTerminal growth set above the riskfree rate, inflating the terminal value
APV and debt-schedule disagree by a wide marginThe bankruptcy cost percentage, or cash netted out of one and not the other
The firm ends up under-levered for no stated reasonA rating floor and an EBIT haircut both applied — they guard the same risk

Signals

GitHub stars
158
Forks
23
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
cost-of-capital-toolkit
Source
github.com/lyndonkl/claude