Banking and Financial Intermediation
Department of Applied Finance
2026-09-10
The last time Australian banks nearly failed
Between 1990 and 1992 Australian banks lost more than A$9 billion before tax, over a third of the entire system’s shareholders’ funds in 1989. Non-performing loans peaked at about 6% of all lending.
Hardly any of it was one bad borrower. It was one sector: commercial property. When Westpac finally revalued its property assets, they were worth about 40% less.
Every state-owned bank in Australia was closed or broken up.1
It still happens
March 2024: New York Community Bancorp took a $1 billion rescue investment after losses on its commercial property book. Same sector, same mistake, thirty years later.
In Week 6 you priced one loan. This week the bank holds thousands of them, and they do not fail independently.
Everything today answers a single question: how much of this risk can the bank actually get rid of?
| The question | What we build | |
|---|---|---|
| 1 | Can we see the concentration? | Migration analysis, concentration limits |
| 2 | Can we diversify it away? | MPT for loans, Moody’s RiskFrontier |
| 3 | Can we sell it? | Credit forwards, options and swaps |
The answer, given away in advance
Question 2 has an uncomfortable answer: only partly. There is a floor below which diversification cannot take you, however many loans you add.
That floor is the reason question 3 exists.
Caution
A large credit risk exposure to a single borrower, or to a group of borrowers exposed to the same risk factor, poses a potential threat to a bank’s safety and soundness.
Which side of the balance sheet?
Concentration can bite on either side. SVB in Week 4 was concentrated in its depositors. This week is about concentration in the loan book.
The hidden-correlation trap
Products with different names, sold by different business units, can still share the same underlying risk. A bank’s “diversified” book may be one bet in disguise.
Two simple models widely used to measure concentration risk:
| AAA-A | BBB-B | CCC-C | Default | |
|---|---|---|---|---|
| AAA-A | 0.85 | 0.10 | 0.04 | 0.01 |
| BBB-B | 0.12 | 0.83 | 0.03 | 0.02 |
| CCC-C | 0.03 | 0.13 | 0.80 | 0.04 |
Table 1, for example, shows the transition probabilities of loans that began the year with a certain credit rating being upgraded/downgraded to a certain rating, or default.
In practice, FIs use migration matrices with many more rating classes (S&P uses 20+).
Migration analysis is also applied to credit card and consumer loan portfolios.
Where migration analysis can mislead you
Discussion
Lehman Brothers was rated A by S&P five days before its September 2008 bankruptcy. What does that tell you about relying on migration analysis alone?
\[ \text{Concentration limit} = \text{Maximum loss as a percentage of capital} \times \frac{1}{\text{Loss rate}} \]
Example
A manager unwilling to lose more than 15% of capital, facing an estimated loss rate of 40% in an industry, sets the limit at \(15\% \times \frac{1}{0.4} = 37.5\%\) of the loan portfolio.
Let’s look at a real Australian bank. Notice anything?
| Sector | $m | % of total |
|---|---|---|
| Residential mortgages | 53,351 | 74.0 |
| Property and construction | 6,904 | 9.6 |
| Other | 3,485 | 4.8 |
| Healthcare | 2,657 | 3.7 |
| Professional services | 1,854 | 2.6 |
| Agriculture | 1,466 | 2.0 |
| Transportation | 853 | 1.2 |
| Manufacturing and mining | 785 | 1.1 |
| Hospitality and accommodation | 658 | 0.9 |
| Other retail lending | 131 | 0.2 |
| Total | 72,144 | 100.0 |
Discussion
74% in residential mortgages. Is BOQ diversified across ten sectors, or is it one bet on Australian house prices wearing ten different hats? This pattern is typical of Australian banks, and it is one reason APRA stress-tests them on a property downturn.
The one idea
The risk of a portfolio is not the average of the risks of the things inside it. It is almost always lower, because the pieces do not all move at the same time.
Markowitz (1952) turned that observation into a procedure:
Why a bank should care
An FI does not choose loans one at a time, it chooses a mix. Two loan books can earn the identical spread and carry very different risk, purely because of who the borrowers are.
What it asks you for
Three numbers per asset: expected return, risk, and how it co-moves with everything else. The third does most of the work, and for loans it is by far the hardest to obtain.
MPT can be used to measure and control an FI’s aggregate credit risk exposure.
Any model that seeks to estimate an efficient frontier for loans needs to determine and measure three things:
Expected return \(R_p\) of a portfolio of \(N\) assets:
\[ R_p = \sum_{i=1}^N X_i R_i \]
where
Variance of returns (or risk) of the portfolio \(\sigma_p^2\) can be calculated as
\[ \begin{aligned} \sigma_p^2 &= \sum_{i=1}^N X_i^2 \sigma^2_i + \sum_{i=1}^N \sum_{\substack{j=1 \\ j\neq i}}^N X_i X_j \sigma_{ij} \\ &= \sum_{i=1}^N X_i^2 \sigma^2_i + \sum_{i=1}^N \sum_{\substack{j=1 \\ j\neq i}}^N X_i X_j \rho_{ij} \sigma_i \sigma_j \end{aligned} \]
where
L = ({R1: 0.0625, s1: Math.sqrt(0.03 * 0.97) * 0.25, R2: 0.056, s2: Math.sqrt(0.02 * 0.98) * 0.20})
portOf = (x, r) => {
const v = x ** 2 * L.s1 ** 2 + (1 - x) ** 2 * L.s2 ** 2
+ 2 * x * (1 - x) * r * L.s1 * L.s2;
return {x: x, sd: Math.sqrt(Math.max(v, 0)) * 100, R: (x * L.R1 + (1 - x) * L.R2) * 100};
}
frontier = Array.from({length: 101}, (_, i) => portOf(i / 100, rho12))
here = portOf(w1, rho12)
html`<div style="font-size:1.05em; margin-bottom:6px;">
R<sub>p</sub> = <b style="color:#A6192E">${here.R.toFixed(2)}%</b> ·
\u03C3<sub>p</sub> = <b style="color:#A6192E">${here.sd.toFixed(2)}%</b>
</div>`Plot.plot({
width: 620, height: 330, marginLeft: 55, marginBottom: 45,
x: {label: "Portfolio risk \u03C3p (%)", domain: [0, 5], grid: true},
y: {label: "Portfolio return Rp (%)", domain: [5.4, 6.4], grid: true},
marks: [
Plot.line(frontier, {x: "sd", y: "R", stroke: "#A6192E", strokeWidth: 2.5}),
Plot.dot([portOf(1, rho12), portOf(0, rho12)], {x: "sd", y: "R", r: 4, fill: "#777"}),
Plot.text([portOf(1, rho12)], {x: "sd", y: "R", text: ["Asset 1 only"], dx: 28, fontSize: 12, fill: "#777"}),
Plot.text([portOf(0, rho12)], {x: "sd", y: "R", text: ["Asset 2 only"], dx: 28, fontSize: 12, fill: "#777"}),
Plot.dot([here], {x: "sd", y: "R", r: 7, fill: "#A6192E"})
]
})The frontier says which mixes are worth considering. To pick one, rank them by return earned per unit of risk, the Sharpe ratio (Sharpe 1966):
\[S=\frac{R_p-r_f}{\sigma_p}\]
The best one is where a line from \(r_f\) just touches the frontier.
A = ({R: 4, sd: 8})
B = ({R: 10, sd: 20})
mix = (w, r) => {
const v = w ** 2 * B.sd ** 2 + (1 - w) ** 2 * A.sd ** 2
+ 2 * w * (1 - w) * r * A.sd * B.sd;
return {w: w, R: w * B.R + (1 - w) * A.R, sd: Math.sqrt(Math.max(v, 0))};
}
curve = Array.from({length: 201}, (_, i) => mix(i / 200, rhoAB))
gmvpW = Math.min(1, Math.max(0,
(A.sd ** 2 - rhoAB * A.sd * B.sd) / (A.sd ** 2 + B.sd ** 2 - 2 * rhoAB * A.sd * B.sd)))
gmvp = mix(gmvpW, rhoAB)
tangency = curve.reduce((best, p) =>
(p.sd > 0.01 && (p.R - rf) / p.sd > (best.R - rf) / best.sd) ? p : best, mix(1, rhoAB))
maxSharpe = (tangency.R - rf) / tangency.sd
efficient = curve.filter(p => p.R >= gmvp.R)
inefficient = curve.filter(p => p.R <= gmvp.R)
cal = [{sd: 0, R: rf}, {sd: 22, R: rf + maxSharpe * 22}]
html`<div style="font-size:1.05em; margin-bottom:4px;">
Best Sharpe = <b style="color:#A6192E">${maxSharpe.toFixed(3)}</b>
at <b>${(tangency.w * 100).toFixed(0)}%</b> in the risky asset
(R = ${tangency.R.toFixed(2)}%, \u03C3 = ${tangency.sd.toFixed(2)}%)
</div>`Plot.plot({
width: 620, height: 350, marginLeft: 55, marginBottom: 45,
x: {label: "Risk \u03C3p (%)", domain: [0, 22], grid: true},
y: {label: "Expected return Rp (%)", domain: [0, 11], grid: true},
marks: [
Plot.line(cal, {x: "sd", y: "R", stroke: "#888", strokeDasharray: "5,4"}),
Plot.line(inefficient, {x: "sd", y: "R", stroke: "#bbb", strokeWidth: 3}),
Plot.line(efficient, {x: "sd", y: "R", stroke: "#A6192E", strokeWidth: 3}),
Plot.dot([A, B], {x: "sd", y: "R", r: 4, fill: "#555"}),
Plot.text([A], {x: "sd", y: "R", text: ["safe asset"], dx: 34, fontSize: 12, fill: "#555"}),
Plot.text([B], {x: "sd", y: "R", text: ["risky asset"], dx: -34, fontSize: 12, fill: "#555"}),
Plot.dot([gmvp], {x: "sd", y: "R", r: 6, fill: "#fff", stroke: "#A6192E", strokeWidth: 2}),
Plot.text([gmvp], {x: "sd", y: "R", text: ["minimum variance"], dx: -6, dy: 18, fontSize: 12, fill: "#A6192E"}),
Plot.dot([tangency], {x: "sd", y: "R", r: 7, fill: "#A6192E"}),
Plot.text([tangency], {x: "sd", y: "R", text: ["best Sharpe"], dx: 8, dy: -14, fontSize: 12, fill: "#A6192E"}),
Plot.dot([{sd: 0, R: rf}], {x: "sd", y: "R", r: 4, fill: "#888"})
]
})Three things to try
Now put loans in it
Nothing above is specific to shares. \(R_i\) and \(\sigma_i\) can also represent the expected return and risk of loans, so that the same picture prices a loan portfolio. That is exactly what RiskFrontier does next.
The floor
Add as many loans as you like. Portfolio risk cannot fall below
\[\sigma_{\text{floor}}=\sigma\sqrt{\rho}\]
At \(\rho=0\) the floor is zero and diversification removes everything. At \(\rho=0.2\) it is \(0.45\sigma\), no matter how many loans you hold.
divCurve = Array.from({length: 200}, (_, i) => {
const N = i + 1;
const v = sigOne ** 2 * (1 / N + (1 - 1 / N) * rhoBar);
return {N: N, sd: Math.sqrt(v)};
})
divFloor = sigOne * Math.sqrt(rhoBar)
sd200 = divCurve[199].sd
html`<div style="font-size:1.05em; margin-bottom:6px;">
One loan alone: <b>${sigOne.toFixed(1)}%</b> ·
200 loans: <b style="color:#A6192E">${sd200.toFixed(2)}%</b> ·
floor: <b>${divFloor.toFixed(2)}%</b>
</div>`Plot.plot({
width: 620, height: 330, marginLeft: 55, marginBottom: 45,
x: {label: "Number of equally weighted loans", domain: [1, 200], grid: true},
y: {label: "Portfolio risk (%)", domain: [0, 10], grid: true},
marks: [
Plot.ruleY([divFloor], {stroke: "#A6192E", strokeDasharray: "4,3"}),
Plot.line(divCurve, {x: "N", y: "sd", stroke: "#A6192E", strokeWidth: 2.5}),
Plot.text([{N: 130, sd: divFloor}],
{x: "N", y: "sd", text: ["systematic risk: cannot be diversified"], dy: -12, fill: "#A6192E", fontSize: 13})
]
})Why this is the most important slide of the week
Everything a bank can do about the downward-sloping part of that curve is diversification. Everything it can do about the flat part is either hold capital against it or sell it to somebody else.
Correlation is what makes the floor high. A book of 200 Australian mortgages is 200 loans and roughly one bet.
The problem: MPT needs three inputs, and for a loan portfolio none of them are directly observable.
flowchart LR
A[Borrower<br/>financials] --> B[<b>Credit Monitor</b><br/>estimates EDF]
B --> C[<b>RiskFrontier</b><br/>portfolio engine]
D[~1,000 systematic<br/>factors via <b>GCORR</b>] --> C
E[Loan terms:<br/>spread, fees, LGD] --> C
C --> F[Portfolio<br/>R<sub>p</sub> and σ<sub>p</sub>]
style B fill:#D6D2C4,stroke:#333
style C fill:#A6192E,color:#fff,stroke:#333
style D fill:#D6D2C4,stroke:#333Two Moody’s models in sequence:
Why not just use historical correlations?
Most loans never trade, so there is no price series to correlate. RiskFrontier instead computes correlations from shared exposure to systematic factors: GCorr distinguishes 61 countries and 49 industries, and runs to close to 1,000 factors in total.
The whole model is just three numbers per loan, fed into standard MPT formulas.
flowchart TB
subgraph inputs [Inputs per loan i]
R["<b>R<sub>i</sub></b>: Expected return<br/>= AIS<sub>i</sub> − EDF<sub>i</sub> × LGD<sub>i</sub>"]
S["<b>σ<sub>i</sub></b>: Unexpected loss<br/>= √[EDF<sub>i</sub>(1−EDF<sub>i</sub>)] × LGD<sub>i</sub>"]
P["<b>ρ<sub>ij</sub></b>: Default correlation<br/>from GCORR factor model"]
end
inputs --> MPT["Standard MPT:<br/>R<sub>p</sub> = Σ X<sub>i</sub>R<sub>i</sub><br/>σ<sub>p</sub><sup>2</sup> = ΣΣ X<sub>i</sub>X<sub>j</sub>ρ<sub>ij</sub>σ<sub>i</sub>σ<sub>j</sub>"]| Symbol | Meaning | Source |
|---|---|---|
| AIS | All-in-drawn spread (loan rate − cost of funds + fees) | Loan contract |
| EDF | Prob. of default in the next year | Credit Monitor |
| LGD | Fraction lost if default occurs | Basel floors or bank estimate1 |
| ρ | Default correlation | GCORR factor model |
Expected return: earn the spread, lose the expected loss.1
\[ \underbrace{R_i}_{\text{net return}} = \underbrace{AIS_i}_{\text{spread + fees}} - \underbrace{EDF_i \times LGD_i}_{E(L_i),\text{ expected loss}} \]
Unexpected loss: default is binomial, so σ has a closed form.2
\[ \sigma_i = UL_i = \underbrace{\sqrt{EDF_i(1-EDF_i)}}_{\sigma \text{ of a 0/1 default event}} \times \underbrace{LGD_i}_{\text{loss if default}} \]
Intuition
Default correlations between two loans cannot be directly observed. GCORR computes them via a factor model: two borrowers are correlated to the extent they share exposure to the same underlying risk factors.
Figure 1: Moody’s GCorr Corporate factor structure
Read Figure 1 this way
Each borrower’s asset return = global economy + region/country + industry + firm-specific noise. Two borrowers are correlated only through the shared branches of the tree. A Sydney miner and a Perth miner share the “Australia + Materials” branches, so ρ is high. A Sydney miner and a Berlin software firm share almost nothing, so ρ is near zero.
Suppose that an FI holds two loans with the following characteristics. Assume that the correlation \(\rho_{12}=-0.25\), what are the return and risk of the portfolio?
| Loan \(i\) | \(X_i\) | Spread between loan rate and FI’s cost of funds | Fees | LGD | EDF |
|---|---|---|---|---|---|
| 1 | 0.6 | 5% | 2% | 25% | 3% |
| 2 | 0.4 | 4.5% | 1.5% | 20% | 2% |
The return and risk on loan 1 are:
\[ \begin{aligned} R_1 &= (0.05+0.02) - (0.03\times0.25) = 0.0625 \\ \sigma_1 &= \sqrt{0.03\times0.97} \times 0.25 = 0.04265 \end{aligned} \]
The return and risk on loan 2 are:
\[ \begin{aligned} R_2 &= (0.045+0.015) - (0.02\times0.2) = 0.056 \\ \sigma_2 &= \sqrt{0.02\times0.98} \times 0.2 = 0.028 \end{aligned} \]
The return and risk of the portfolio are then:
\[ \begin{aligned} R_p &= 0.6\times 0.0625 + 0.4\times 0.056 = 0.0599 \text{ or } 5.99\% \\ \sigma_p^2 &= (0.6)^2(0.04265)^2 + (0.4)^2(0.028)^2 + 2(0.6)(0.4)(-0.25)(0.04265)(0.028) = 0.0006369 \\ \sigma_p &= \sqrt{0.0006369} = 0.0252 = 2.52\% \end{aligned} \]
| Bank profile during GFC | Failure rate |
|---|---|
| Construction loans > 100% of capital | 13% |
| Exceeded BOTH construction AND total CRE criteria | 23% |
| Did not exceed either criterion | 0.5% |
Takeaway
Banks that breached both concentration criteria failed at 46 times the rate of banks that breached neither. Concentration guidance is advisory. The statistical case for it is not.
Why this innovation matters
Before credit derivatives (pre-1990s), the only way a bank could reduce credit exposure to a big client was to refuse the loan or sell it, both of which damage the relationship. Credit derivatives let the bank say “yes” and still cap its downside.
| Market outcome | Long position (hedger) | Short position |
|---|---|---|
| Spread widens → credit quality worsens | Gains (receives payment) | Loses (makes payment) |
| Spread tightens → credit quality improves | Loses (makes payment) | Gains (receives payment) |
where
\(\phi_F\) is the credit spread on which the credit forward contract is written
\(\phi_T\) is the actual credit spread on the bond when the credit forward matures
\(MD\) is the modified duration on the benchmark bond
\(A\) is the principal amount of the forward agreement
Long position protects against borrower credit quality getting worse
Short position benefits if borrower credit improves
Acts like a put-style hedge for lenders
Credit options are a small corner of the market. Credit default swaps make up the large majority of US bank credit-derivative notional (78% in the first quarter of 2026), with total return swaps and credit options sharing the remainder.
The most important, and most controversial, credit derivative.
Explosive growth, then regulatory pushback:
| Date | US bank credit-derivative notional | Note |
|---|---|---|
| 2000 | $0.43 trillion | Market in its infancy |
| March 2008 | $16.44 trillion | Pre-GFC peak |
| September 2011 | $15.66 trillion | Still near the peak |
| September 2021 | $3.9 trillion | Post-Dodd-Frank trough |
| March 2026 | $6.7 trillion | CDS $5.2tn, 78% of the total |
Down 75%, then back up
Dodd-Frank (2010) pushed standardised CDS onto central clearinghouses, with margin requirements that made speculative positions far more expensive. Bilateral dealer books shrank by roughly three quarters from the 2008 peak.
Note the last row. The market has been growing again since. Central clearing did not kill CDS, it changed who bears the counterparty risk.
Why CDS exist:
We examine two types of credit swaps:
A total return swap involves swapping an obligation to pay interest at a specified fixed or floating rate for payments representing the total return on a loan or a bond (interest and principal value changes) of a specified amount.
The figure below illustrates a total return swap.1

The FI lender pays a fixed annual rate \(f\) plus changes in the market value of the loan and receives a variable rate payment (historically 1-year LIBOR; post-2023 SOFR in USD, AONIA/BBSW in AUD since LIBOR’s cessation in June 2023).
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(8003)
# Parameters for the TRS
notional = 1_000_000 # Notional amount (1 million)
fixed_rate = 0.03 # Fixed annual rate paid by the TRS receiver (3%)
asset_volatility = 0.1 # Volatility of the asset's return (10%)
years = 5 # Time period of the TRS (5 years)
periods_per_year = 4 # Payments per year (quarterly)
# Generate the timeline for TRS payments
def generate_trs_timeline(
notional, fixed_rate, asset_volatility, years, periods_per_year=4
):
# Set up the timeline with quarterly periods
total_periods = periods_per_year * years
time_points = np.arange(1, total_periods + 1) / periods_per_year
# Calculate the periodic fixed payment amount (quarterly)
periodic_fixed_payment = (
-notional * fixed_rate / periods_per_year
) # Fixed payment made by TRS receiver
# Simulate random returns on the asset
asset_returns = np.random.normal(loc=0, scale=asset_volatility, size=total_periods)
# Initialize lists to store the results
total_returns = np.zeros(total_periods)
payments = np.zeros(total_periods)
for period in range(total_periods):
# Total return on the asset (positive or negative)
total_return = notional * asset_returns[period]
total_returns[period] = total_return
# Net payment for the TRS receiver (positive if receiving total return, minus fixed payment)
payments[period] = total_return + periodic_fixed_payment
return time_points, payments, total_returns
# Simulate TRS payments with different asset volatilities
volatilities = [0.05] # Different volatilities to simulate
simulations = {}
for vol in volatilities:
timeline, payments, total_returns = generate_trs_timeline(
notional, fixed_rate, vol, years, periods_per_year
)
simulations[vol] = payments
# Plot the timelines as bar charts
plt.figure(figsize=(12, 8))
bar_width = 0.2 # Width of each bar
# Generate bar charts for each volatility
for idx, (vol, payments) in enumerate(simulations.items()):
plt.bar(
timeline + idx * bar_width * 0.1,
payments,
width=bar_width,
color="#A6192E",
label=f"Volatility: {vol*100:.0f}%",
)
# Customize the plot
plt.title(f"{years}-Year Total Return Swap Payments Example")
plt.xlabel("Time (Years)")
plt.ylabel("Net Payment Amount ($)")
plt.axhline(0, color="black", linewidth=1)
plt.grid(True)
plt.legend()
# Show the plot
plt.show()Interest-rate sensitive element stripped out leaving only the credit risk.
Similar to buying an insurance:
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(8003)
# Parameters for the CDS
notional = 1_000_000 # Notional amount (1 million)
cds_spread = 0.02 # Annual CDS spread (2%)
recovery_rate = 0.9 # Recovery rate in case of default (90%)
years = 5 # Time period of the CDS (5 years)
periods_per_year = 4 # Payments per year (quarterly)
# Generate the timeline for CDS payments
def generate_cds_timeline(
notional, cds_spread, default_probability, recovery_rate, years, periods_per_year=4
):
# Set up the timeline with quarterly periods
total_periods = periods_per_year * years
time_points = np.arange(1, total_periods + 1) / periods_per_year
# Calculate the periodic payment amount (quarterly)
periodic_payment = (
-notional * cds_spread / periods_per_year
) # Negative for payments
# Initialize lists to store the results
payments = np.zeros(total_periods)
# Simulate payments and default events
for period in range(total_periods):
if np.random.rand() < default_probability / periods_per_year:
# Default occurs at this period
default_loss = notional * (
1 - recovery_rate
) # Positive for the payment received on default
payments[period] = default_loss
payments[period + 1 :] = 0 # No more payments after default
break
else:
# Regular payment
payments[period] = periodic_payment
return time_points, payments
# Simulate multiple series with different default probabilities
default_probabilities = [0.02, 0.1] # Different default probabilities
colors = ["#D6D2C4", "#A6192E"]
simulations = {}
# Generate timelines for each default probability
for prob in default_probabilities:
timeline, payments = generate_cds_timeline(
notional, cds_spread, prob, recovery_rate, years, periods_per_year
)
simulations[prob] = payments
# Plot the timelines as bar charts
plt.figure(figsize=(12, 8))
bar_width = 0.2 # Width of each bar
# Generate bar charts for each probability
for idx, (prob, payments) in enumerate(simulations.items()):
plt.bar(
timeline + idx * bar_width * 0.1,
payments,
width=bar_width,
color=colors[idx],
label=f"Default Probability: {prob*100:.0f}%",
)
plt.title(f"{years}-Year Pure CDS Payments Example")
plt.xlabel("Time (Years)")
plt.ylabel("Payment Amount ($)")
plt.axhline(0, color="black", linewidth=1)
plt.grid(True)
plt.legend()
plt.show()AFIN8003 Banking and Financial Intermediation