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:
-
Maintain a stable Net Interest Income (NII).
-
Protect the Economic Value of Equity (EVE).
-
Ensure adequate liquidity under stress conditions.
-
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:
-
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).
-
-
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
# =================================================================== # 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
-
Apply duration gap analysis to a real bank balance sheet.
-
Calculate LCR and NSFR using actual bank data (publicly available from regulatory filings).
-
Implement liquidity stress testing with more granular cash flow projections.
-
Learn about Behavioural Modelling for deposits (decay rates, stickiness).
-
Study IFRS 9 and CECL for expected credit loss provisioning (linked to ALM).
-
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]