SECTION 1: LEARNING OBJECTIVES

By the end of this lesson, you will be able to:

  • Define Asset-Liability Management (ALM) and its importance in banking.

  • Understand the key ALM metrics – Net Interest Income (NII), Economic Value of Equity (EVE), and Duration Gap.

  • Compute duration gap and understand its impact on a bank’s balance sheet under interest rate changes.

  • Define liquidity risk and its two components: funding liquidity risk and market liquidity risk.

  • Understand the Basel III liquidity requirements – Liquidity Coverage Ratio (LCR) and Net Stable Funding Ratio (NSFR).

  • Calculate the LCR using the stock of High-Quality Liquid Assets (HQLA) and the total net cash outflows.

  • Calculate the NSFR as a measure of structural funding stability.

  • Apply liquidity stress testing to assess the impact of liquidity shocks.

  • Use Python to implement duration gap analysis, LCR, and NSFR calculations.


SECTION 2: WHAT IS ASSET-LIABILITY MANAGEMENT (ALM)?

ALM is the process of managing the risks that arise from the mismatches between a bank’s assets (loans, investments) and liabilities (deposits, borrowings).

Why ALM matters:

  • Banks borrow short-term (deposits) and lend long-term (mortgages, corporate loans) – this is the maturity transformation function.

  • This creates exposure to interest rate risk (changes in rates affect asset and liability values differently) and liquidity risk (the need to roll over short-term funding).

Key ALM objectives:

  1. Maintain a stable Net Interest Income (NII).

  2. Protect the Economic Value of Equity (EVE).

  3. Ensure adequate liquidity under stress conditions.

  4. Optimise the balance sheet structure (assets, liabilities, capital).

The balance sheet approach:

  • Assets: Loans, securities, cash, reserves.

  • Liabilities: Deposits, wholesale funding, short-term borrowings.

  • Equity: Share capital, retained earnings.

The difference between the interest rate sensitivities of assets and liabilities determines the bank’s exposure.


SECTION 3: INTEREST RATE RISK – DURATION GAP

Duration measures the sensitivity of a financial instrument’s price to changes in interest rates.

Duration=−1PdPdy

For a bond, the Macaulay duration is the weighted average time to receive cash flows:

D=∑t=1Tt⋅CFt⋅(1+y)−t∑t=1TCFt⋅(1+y)−t

Modified duration (used for interest rate risk):

Dmod=D1+y

Duration Gap = Weighted Average Duration of Assets (DA) – Weighted Average Duration of Liabilities (DL) × (Liabilities / Assets)

Duration Gap=DA−DL×LA

Change in Economic Value of Equity (ΔEVE):

ΔEVE=−Duration Gap×A×Δy

Interpretation:

  • Positive duration gap: Assets have longer duration than liabilities → rates rise → EVE falls.

  • Negative duration gap: Liabilities have longer duration → rates rise → EVE increases.

  • Zero duration gap: Assets and liabilities are matched → EVE is immunised (no interest rate risk).


SECTION 4: LIQUIDITY RISK – DEFINITION AND TYPES

Liquidity risk is the risk that a bank will not be able to meet its obligations as they fall due without incurring unacceptable losses.

Two types of liquidity risk:

 
 
Type Definition Example
Funding Liquidity Risk The risk that the bank cannot obtain sufficient funding to meet its obligations. Depositors withdraw funds; the bank cannot roll over wholesale funding.
Market Liquidity Risk The risk that the bank cannot sell assets quickly without significant price concessions. Fire sale of assets during a crisis (e.g., 2008, when mortgage-backed securities could not be sold).

Key metrics:

  • Liquidity Coverage Ratio (LCR): Short-term liquidity (30-day horizon).

  • Net Stable Funding Ratio (NSFR): Structural funding (1-year horizon).


SECTION 5: LIQUIDITY COVERAGE RATIO (LCR) – BASEL III

Definition: The LCR ensures that banks have enough High-Quality Liquid Assets (HQLA) to cover net cash outflows over a 30-day stress scenario.

LCR=Stock of HQLATotal Net Cash Outflows over 30 days≥100%

Components:

  1. HQLA (High-Quality Liquid Assets):

    • Level 1: Cash, central bank reserves, government bonds (risk weight 0%). No haircut.

    • Level 2A: Highly liquid corporate and covered bonds (20% haircut).

    • Level 2B: Lower-quality assets (e.g., certain corporate bonds, equities with 50% haircut).

    • There is a cap on Level 2 assets (40% of total HQLA).

  2. Total Net Cash Outflows:

    • Outflows: Deposit withdrawals, wholesale funding maturing, undrawn commitments.

    • Inflows: Expected receipts from performing assets (capped at 75% of outflows).

Calculation:

Total Net Cash Outflows=∑Outflows−min⁡(∑Inflows,0.75×∑Outflows)


SECTION 6: NET STABLE FUNDING RATIO (NSFR) – BASEL III

Definition: The NSFR ensures that banks maintain a stable funding profile over a 1-year horizon.

NSFR=Available Stable Funding (ASF)Required Stable Funding (RSF)≥100%

Components:

  • ASF: The amount of stable funding available (e.g., equity, long-term debt, stable deposits).

  • RSF: The amount of stable funding required by the bank’s assets and off-balance sheet exposures.

ASF factors:

  • Regulatory capital (100% stable)

  • Long-term debt (>1 year) (100%)

  • Retail deposits (90-95% depending on stability)

  • Wholesale deposits (50-100% depending on maturity)

RSF factors:

  • Cash (0%)

  • Government bonds (0-20%)

  • Corporate loans (50-100% depending on maturity and quality)

  • Residential mortgages (50-65%)


SECTION 7: IMPLEMENTATION IN PYTHON – ALM AND LIQUIDITY RISK

python
# ===================================================================
# MODULE 5, LESSON 8: ASSET-LIABILITY MANAGEMENT AND LIQUIDITY RISK
# ===================================================================

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from scipy.optimize import minimize
import warnings
warnings.filterwarnings('ignore')

# Set style
sns.set_style("whitegrid")
np.random.seed(42)

print("="*70)
print("ASSET-LIABILITY MANAGEMENT AND LIQUIDITY RISK")
print("="*70)

# ----------------------------------------------------------------
# PART A: SIMPLIFIED BANK BALANCE SHEET
# ----------------------------------------------------------------

# Assets
assets = {
    'Cash': 100,
    'Government Bonds': 200,
    'Corporate Loans': 400,
    'Residential Mortgages': 300,
    'Other Assets': 50
}
total_assets = sum(assets.values())

# Liabilities
liabilities = {
    'Retail Deposits': 350,
    'Wholesale Deposits': 250,
    'Short-term Borrowings': 150,
    'Long-term Debt': 150,
    'Other Liabilities': 50
}
total_liabilities = sum(liabilities.values())

# Equity
equity = total_assets - total_liabilities

balance_sheet = pd.DataFrame({
    'Item': list(assets.keys()) + list(liabilities.keys()) + ['Equity'],
    'Category': ['Asset']*len(assets) + ['Liability']*len(liabilities) + ['Equity'],
    'Amount': list(assets.values()) + list(liabilities.values()) + [equity]
})

print("Simplified Bank Balance Sheet:")
print(balance_sheet.to_string(index=False))
print(f"\nTotal Assets: ${total_assets:,.0f}M")
print(f"Total Liabilities: ${total_liabilities:,.0f}M")
print(f"Equity: ${equity:,.0f}M")
print(f"Equity Ratio: {equity/total_assets:.2%}")

# ----------------------------------------------------------------
# PART B: DURATION GAP ANALYSIS
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART B: Duration Gap Analysis")
print("-"*60)

# Assign durations to balance sheet items
duration_assets = {
    'Cash': 0.0,
    'Government Bonds': 3.0,
    'Corporate Loans': 4.5,
    'Residential Mortgages': 6.0,
    'Other Assets': 1.0
}

duration_liabilities = {
    'Retail Deposits': 1.0,
    'Wholesale Deposits': 0.5,
    'Short-term Borrowings': 0.3,
    'Long-term Debt': 4.0,
    'Other Liabilities': 0.5
}

# Weighted average duration of assets
da = sum(assets[k] * duration_assets[k] for k in assets) / total_assets
print(f"Asset Duration (DA): {da:.2f} years")

# Weighted average duration of liabilities
dl = sum(liabilities[k] * duration_liabilities[k] for k in liabilities) / total_liabilities
print(f"Liability Duration (DL): {dl:.2f} years")

# Duration Gap
duration_gap = da - dl * (total_liabilities / total_assets)
print(f"Duration Gap: {duration_gap:.2f} years")

# Impact of a 1% parallel shift in interest rates
rate_shift = 0.01  # 1%
delta_eve = -duration_gap * total_assets * rate_shift
print(f"\nImpact of 1% rate increase on EVE: ${delta_eve:,.2f}M")

# Sensitivity analysis: range of rate changes
rate_changes = np.linspace(-0.03, 0.03, 50)
eve_changes = -duration_gap * total_assets * rate_changes

fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(rate_changes * 100, eve_changes, 'b-', linewidth=2)
ax.axhline(0, color='black', linestyle='-', alpha=0.3)
ax.axvline(0, color='black', linestyle='-', alpha=0.3)
ax.set_xlabel('Interest Rate Change (%)')
ax.set_ylabel('Change in Economic Value of Equity ($M)')
ax.set_title('EVE Sensitivity to Interest Rate Changes')
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('eve_sensitivity.png', dpi=300)
plt.show()

# ----------------------------------------------------------------
# PART C: DURATION GAP MANAGEMENT – HEDGING STRATEGY
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART C: Duration Gap Management")
print("-"*60)

# Example: To immunise the balance sheet, the bank could adjust the duration of assets
# or liabilities. We'll find the required change in asset duration.
target_gap = 0.0
required_da = target_gap + dl * (total_liabilities / total_assets)
print(f"Required Asset Duration to achieve zero duration gap: {required_da:.2f} years")
print(f"Current Asset Duration: {da:.2f} years")
print(f"Needed adjustment: {required_da - da:.2f} years")

# This could be achieved by:
# - Reducing holdings of long-duration assets (e.g., mortgages)
# - Increasing holdings of short-duration assets (e.g., cash, short-term bonds)
# - Using interest rate swaps or derivatives

print("\nHedging Strategies:")
print("1. Use Interest Rate Swaps: Pay fixed, receive floating.")
print("2. Use Interest Rate Futures or Options.")
print("3. Adjust the asset mix (sell long-duration, buy short-duration).")

# ----------------------------------------------------------------
# PART D: LIQUIDITY COVERAGE RATIO (LCR) CALCULATION
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART D: Liquidity Coverage Ratio (LCR)")
print("-"*60)

# HQLA calculation
hqla_level1 = assets['Cash'] + assets['Government Bonds'] * 1.0  # Level 1 (0% haircut)
hqla_level2a = assets['Corporate Loans'] * 0.5  # Assume 50% are Level 2A (20% haircut)
hqla_level2b = assets['Residential Mortgages'] * 0.2  # Assume 20% are Level 2B (50% haircut)

# Capped at 40% of total HQLA for Level 2
hqla_level2 = hqla_level2a + hqla_level2b
hqla_cap = 0.4 * (hqla_level1 + hqla_level2)
if hqla_level2 > hqla_cap:
    hqla_level2_adj = hqla_cap
else:
    hqla_level2_adj = hqla_level2

hqla_total = hqla_level1 + hqla_level2_adj

print("HQLA Components:")
print(f"  Level 1: ${hqla_level1:,.0f}M")
print(f"  Level 2 (capped): ${hqla_level2_adj:,.0f}M")
print(f"  Total HQLA: ${hqla_total:,.0f}M")

# Cash outflows (30-day stress scenario)
outflows = {
    'Retail Deposits (stable)': liabilities['Retail Deposits'] * 0.05,  # 5% run-off
    'Retail Deposits (unstable)': liabilities['Retail Deposits'] * 0.1 * 0.5,  # 10% run-off for half
    'Wholesale Deposits': liabilities['Wholesale Deposits'] * 0.4,  # 40% run-off
    'Short-term Borrowings': liabilities['Short-term Borrowings'] * 1.0,  # 100% roll-over risk
    'Undrawn Commitments': 20  # Example: undrawn credit lines
}
total_outflows = sum(outflows.values())

# Cash inflows (expected receipts)
inflows = {
    'Loan Repayments': 30,
    'Securities Maturities': 20,
    'Other Inflows': 10
}
total_inflows = sum(inflows.values())

# Net cash outflows (inflows capped at 75% of outflows)
inflow_cap = 0.75 * total_outflows
net_outflows = total_outflows - min(total_inflows, inflow_cap)

lcr = hqla_total / net_outflows

print("\n30-Day Net Cash Outflows:")
print(f"  Total Outflows: ${total_outflows:,.0f}M")
print(f"  Total Inflows: ${total_inflows:,.0f}M")
print(f"  Net Outflows (capped): ${net_outflows:,.0f}M")
print(f"\nLiquidity Coverage Ratio (LCR): {lcr:.2f} ({lcr*100:.0f}%)")
if lcr >= 1.0:
    print("  ✓ LCR meets regulatory requirement (≥ 100%)")
else:
    print("  ⚠ LCR is below 100% – action required")

# ----------------------------------------------------------------
# PART E: NET STABLE FUNDING RATIO (NSFR) CALCULATION
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART E: Net Stable Funding Ratio (NSFR)")
print("-"*60)

# Available Stable Funding (ASF)
asf_factors = {
    'Equity': 1.0,
    'Long-term Debt': 1.0,
    'Retail Deposits (stable)': 0.95,
    'Retail Deposits (unstable)': 0.90,
    'Wholesale Deposits': 0.50,
    'Short-term Borrowings': 0.0,
    'Other Liabilities': 0.0
}

asf_amount = (
    equity * asf_factors['Equity'] +
    liabilities['Long-term Debt'] * asf_factors['Long-term Debt'] +
    liabilities['Retail Deposits'] * 0.5 * asf_factors['Retail Deposits (stable)'] +
    liabilities['Retail Deposits'] * 0.5 * asf_factors['Retail Deposits (unstable)'] +
    liabilities['Wholesale Deposits'] * asf_factors['Wholesale Deposits']
)

print(f"Available Stable Funding (ASF): ${asf_amount:,.0f}M")

# Required Stable Funding (RSF)
rsf_factors = {
    'Cash': 0.0,
    'Government Bonds': 0.05,
    'Corporate Loans': 0.65,
    'Residential Mortgages': 0.50,
    'Other Assets': 0.50
}

rsf_amount = (
    assets['Cash'] * rsf_factors['Cash'] +
    assets['Government Bonds'] * rsf_factors['Government Bonds'] +
    assets['Corporate Loans'] * rsf_factors['Corporate Loans'] +
    assets['Residential Mortgages'] * rsf_factors['Residential Mortgages'] +
    assets['Other Assets'] * rsf_factors['Other Assets']
)

print(f"Required Stable Funding (RSF): ${rsf_amount:,.0f}M")

nsfr = asf_amount / rsf_amount
print(f"\nNet Stable Funding Ratio (NSFR): {nsfr:.2f} ({nsfr*100:.0f}%)")
if nsfr >= 1.0:
    print("  ✓ NSFR meets regulatory requirement (≥ 100%)")
else:
    print("  ⚠ NSFR is below 100% – action required")

# ----------------------------------------------------------------
# PART F: LIQUIDITY STRESS TESTING
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART F: Liquidity Stress Testing")
print("-"*60)

# Define stress scenarios
liquidity_scenarios = {
    'Baseline': {'deposit_runoff': 0.05, 'funding_rollover': 0.1, 'asset_sale': 0.0},
    'Moderate Stress': {'deposit_runoff': 0.15, 'funding_rollover': 0.3, 'asset_sale': 0.05},
    'Severe Stress': {'deposit_runoff': 0.30, 'funding_rollover': 0.6, 'asset_sale': 0.10},
    'Extreme Stress': {'deposit_runoff': 0.50, 'funding_rollover': 0.9, 'asset_sale': 0.20}
}

# Function to compute liquidity gap under stress
def liquidity_gap(scenario, assets, liabilities, hqla):
    """Compute liquidity gap under a stress scenario."""
    runoff = scenario['deposit_runoff'] * liabilities['Retail Deposits']
    wholesale_runoff = scenario['funding_rollover'] * liabilities['Wholesale Deposits']
    short_term_runoff = scenario['funding_rollover'] * liabilities['Short-term Borrowings']
    
    total_outflows = runoff + wholesale_runoff + short_term_runoff
    
    # Asset sales (liquid assets sold at a discount)
    asset_sale_need = max(0, total_outflows - hqla)
    if asset_sale_need > 0:
        # Sale at a discount
        sale_proceeds = asset_sale_need * (1 - scenario['asset_sale'])
        liquidity_gap = total_outflows - hqla - sale_proceeds
    else:
        liquidity_gap = total_outflows - hqla
    
    return liquidity_gap

# Run stress scenarios
stress_results_liquidity = []
for name, scenario in liquidity_scenarios.items():
    gap = liquidity_gap(scenario, assets, liabilities, hqla_total)
    stress_results_liquidity.append({
        'Scenario': name,
        'Liquidity Gap': gap,
        'LCR': hqla_total / max(0.1, (gap + hqla_total))  # Approx LCR under stress
    })

liquidity_stress_df = pd.DataFrame(stress_results_liquidity)
print("\nLiquidity Stress Test Results:")
print(liquidity_stress_df.to_string(index=False))

# Visualise
fig, ax = plt.subplots(figsize=(10, 6))
x = np.arange(len(liquidity_stress_df))
bars = ax.bar(x, liquidity_stress_df['Liquidity Gap'], 
              color=['green', 'yellow', 'orange', 'red'])
ax.axhline(0, color='black', linestyle='-', alpha=0.5)
ax.set_xticks(x)
ax.set_xticklabels(liquidity_stress_df['Scenario'])
ax.set_ylabel('Liquidity Gap ($M)')
ax.set_title('Liquidity Stress Testing – Gap under Scenarios')
for bar, val in zip(bars, liquidity_stress_df['Liquidity Gap']):
    ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 5, 
            f'${val:.0f}M', ha='center', va='bottom')
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('liquidity_stress_test.png', dpi=300)
plt.show()

# ----------------------------------------------------------------
# PART G: LIQUIDITY RISK MITIGATION
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART G: Liquidity Risk Mitigation Strategies")
print("-"*60)

print("""
Key Mitigation Strategies:

1. Maintain adequate HQLA (Level 1 assets: cash, central bank reserves, government bonds).

2. Diversify funding sources: retail deposits, wholesale funding, securitisation, central bank facilities.

3. Establish contingency funding plans (CFP) – pre-arranged lines of credit.

4. Monitor intraday liquidity and early warning indicators (EWIs).

5. Implement stress testing and scenario analysis regularly.

6. Manage collateral and ensure sufficient unencumbered assets.

7. Maintain a liquidity buffer well above regulatory minimums.

8. Ensure the maturity profile of assets and liabilities is matched as much as possible.
""")

# ----------------------------------------------------------------
# PART H: ALM AND LIQUIDITY RISK REPORTING
# ----------------------------------------------------------------

print("\n" + "="*70)
print("PART H: ALM and Liquidity Risk Summary Report")
print("="*70)

report = pd.DataFrame({
    'Metric': [
        'Total Assets',
        'Total Liabilities',
        'Equity',
        'Equity Ratio',
        'Asset Duration (DA)',
        'Liability Duration (DL)',
        'Duration Gap',
        'EVE Sensitivity (1% rate shift)',
        'HQLA',
        'Net Cash Outflows (30-day)',
        'Liquidity Coverage Ratio (LCR)',
        'Available Stable Funding (ASF)',
        'Required Stable Funding (RSF)',
        'Net Stable Funding Ratio (NSFR)'
    ],
    'Value': [
        f"${total_assets:,.0f}M",
        f"${total_liabilities:,.0f}M",
        f"${equity:,.0f}M",
        f"{equity/total_assets:.2%}",
        f"{da:.2f} years",
        f"{dl:.2f} years",
        f"{duration_gap:.2f} years",
        f"${delta_eve:,.0f}M",
        f"${hqla_total:,.0f}M",
        f"${net_outflows:,.0f}M",
        f"{lcr:.2f} ({lcr*100:.0f}%)",
        f"${asf_amount:,.0f}M",
        f"${rsf_amount:,.0f}M",
        f"{nsfr:.2f} ({nsfr*100:.0f}%)"
    ]
})
print(report.to_string(index=False))

print("\n" + "="*70)
print("END OF LESSON 8 – MODULE 5")
print("END OF MODULE 5")
print("="*70)

SECTION 8: REGULATORY REQUIREMENTS SUMMARY

 
 
Regulation Requirement Key Element
Basel III (LCR) LCR ≥ 100% (30-day horizon). HQLA / net cash outflows.
Basel III (NSFR) NSFR ≥ 100% (1-year horizon). ASF / RSF.
Basel III (IRRBB) Interest Rate Risk in the Banking Book. EVE and NII sensitivity.
EBA Guidelines ALM and liquidity risk management. Stress testing, contingency plans.
SR 11-7 Model validation for ALM models. Duration and liquidity models.

SECTION 9: SUMMARY FOR THE DATA PRACTITIONER

  • ALM balances assets and liabilities to manage interest rate and liquidity risk.

  • Duration gap measures the sensitivity of equity value to interest rate changes.

  • LCR ensures banks can survive a 30-day liquidity stress event.

  • NSFR promotes long-term funding stability.

  • Stress testing evaluates liquidity under extreme scenarios.

  • Mitigation strategies include HQLA buffers, funding diversification, and contingency plans.

  • ALM and liquidity risk are key regulatory priorities – compliance with Basel III is mandatory.


SECTION 10: RECOMMENDED NEXT STEPS

  1. Apply duration gap analysis to a real bank balance sheet.

  2. Calculate LCR and NSFR using actual bank data (publicly available from regulatory filings).

  3. Implement liquidity stress testing with more granular cash flow projections.

  4. Learn about Behavioural Modelling for deposits (decay rates, stickiness).

  5. Study IFRS 9 and CECL for expected credit loss provisioning (linked to ALM).

  6. Prepare for the next module on Advanced Topics: Natural Language Processing and AI in Finance.


[END OF LESSON 8 – MODULE 5]
[END OF MODULE 5]