|
1 | 1 | # sovereign_debt_xl |
2 | | -PyXLL is an Excel add-in exposing Python UDFs for sovereign debt analysis: debt sustainability modeling, yield curve forecasting, rolling averages, and portfolio indexing . No VBA, no context switching. |
| 2 | + |
| 3 | +A [PyXLL](https://www.pyxll.com/) Excel add-in that exposes Python UDFs for sovereign debt analysis: debt sustainability modelling, yield curve fitting, risk metrics, macro-financial linkages, and inline Matplotlib charts — directly in Excel cells with no VBA, no context switching. |
| 4 | + |
| 5 | +--- |
| 6 | + |
| 7 | +## Table of Contents |
| 8 | + |
| 9 | +- [Project layout](#project-layout) |
| 10 | +- [Requirements](#requirements) |
| 11 | +- [Installation](#installation) |
| 12 | +- [PyXLL configuration](#pyxll-configuration) |
| 13 | +- [Function reference](#function-reference) |
| 14 | + - [Averaging & statistics](#averaging--statistics) |
| 15 | + - [Indexing & normalisation](#indexing--normalisation) |
| 16 | + - [Forecasting](#forecasting) |
| 17 | + - [Modelling utilities](#modelling-utilities) |
| 18 | + - [Utilities](#utilities) |
| 19 | + - [Fiscal sustainability](#fiscal-sustainability) |
| 20 | + - [Credit risk](#credit-risk) |
| 21 | + - [Yield curve](#yield-curve) |
| 22 | + - [Reserves & external sector](#reserves--external-sector) |
| 23 | + - [Stress testing](#stress-testing) |
| 24 | + - [Amortisation & financing](#amortisation--financing) |
| 25 | + - [Political risk & ESG](#political-risk--esg) |
| 26 | + - [Contagion](#contagion) |
| 27 | + - [Debt composition](#debt-composition) |
| 28 | + - [Macro-financial linkages](#macro-financial-linkages) |
| 29 | + - [Market microstructure](#market-microstructure) |
| 30 | + - [IMF framework](#imf-framework) |
| 31 | + - [Event studies](#event-studies) |
| 32 | + - [Plotting (inline charts)](#plotting-inline-charts) |
| 33 | +- [Running tests](#running-tests) |
| 34 | + |
| 35 | +--- |
| 36 | + |
| 37 | +## Project layout |
| 38 | + |
| 39 | +``` |
| 40 | +sovereign_debt_xl/ ← repo root |
| 41 | +├── sovereign_debt_xl/ ← installable Python package |
| 42 | +│ ├── __init__.py ← re-exports every submodule |
| 43 | +│ ├── _coerce.py ← internal input-coercion helpers |
| 44 | +│ ├── amortization.py |
| 45 | +│ ├── averaging.py |
| 46 | +│ ├── contagion.py |
| 47 | +│ ├── credit_risk.py |
| 48 | +│ ├── debt_composition.py |
| 49 | +│ ├── event_studies.py |
| 50 | +│ ├── fiscal.py |
| 51 | +│ ├── forecasting.py |
| 52 | +│ ├── imf_framework.py |
| 53 | +│ ├── indexing.py |
| 54 | +│ ├── macro_financial.py |
| 55 | +│ ├── market_microstructure.py |
| 56 | +│ ├── modeling.py |
| 57 | +│ ├── plots.py ← inline Matplotlib chart UDFs |
| 58 | +│ ├── political_esg.py |
| 59 | +│ ├── reserves.py |
| 60 | +│ ├── stress.py |
| 61 | +│ ├── utils.py |
| 62 | +│ └── yield_curve.py |
| 63 | +├── test_*.py ← pytest test suite (repo root) |
| 64 | +├── conftest.py ← pyxll mock for tests |
| 65 | +├── pyproject.toml |
| 66 | +├── pyxll.cfg |
| 67 | +└── requirements.txt |
| 68 | +``` |
| 69 | + |
| 70 | +All source code lives inside `sovereign_debt_xl/` so that `pip install -e .` correctly packages the module. Tests remain at the repo root so pytest discovers them automatically. |
| 71 | + |
| 72 | +--- |
| 73 | + |
| 74 | +## Requirements |
| 75 | + |
| 76 | +- Python ≥ 3.11 |
| 77 | +- [PyXLL](https://www.pyxll.com/) (Excel add-in runtime; not required for running tests) |
| 78 | +- Python dependencies (installed automatically via `pip install`): |
| 79 | + - `numpy`, `pandas`, `scipy`, `statsmodels`, `scikit-learn`, `matplotlib` |
| 80 | + |
| 81 | +--- |
| 82 | + |
| 83 | +## Installation |
| 84 | + |
| 85 | +```bash |
| 86 | +# 1. Install Python dependencies |
| 87 | +pip install -r requirements.txt |
| 88 | + |
| 89 | +# 2. Install the package in editable mode |
| 90 | +pip install -e . |
| 91 | +``` |
| 92 | + |
| 93 | +`pyproject.toml` declares `sovereign_debt_xl` as the package root so setuptools will find and install everything under `sovereign_debt_xl/`. |
| 94 | + |
| 95 | +--- |
| 96 | + |
| 97 | +## PyXLL configuration |
| 98 | + |
| 99 | +`pyxll.cfg` loads the entire package through a single entry: |
| 100 | + |
| 101 | +```ini |
| 102 | +[PYXLL] |
| 103 | +modules= |
| 104 | + sovereign_debt_xl |
| 105 | +``` |
| 106 | + |
| 107 | +`sovereign_debt_xl/__init__.py` re-exports every submodule, so PyXLL discovers all UDFs automatically when the add-in loads. |
| 108 | + |
| 109 | +--- |
| 110 | + |
| 111 | +## Function reference |
| 112 | + |
| 113 | +All UDFs follow two naming conventions: |
| 114 | + |
| 115 | +| Prefix | Scope | |
| 116 | +|--------|-------| |
| 117 | +| `SOV_` | Analytic / numerical functions that return values or arrays | |
| 118 | +| `SDXL_PLOT_` | Plotting functions that return an inline PNG image | |
| 119 | + |
| 120 | +### Averaging & statistics |
| 121 | + |
| 122 | +| Function | Description | |
| 123 | +|----------|-------------| |
| 124 | +| `SOV_WEIGHTED_AVERAGE(values, weights)` | Weighted arithmetic mean | |
| 125 | +| `SOV_ROLLING_AVERAGE(values, window)` | Rolling / moving average | |
| 126 | +| `SOV_TRIMMED_MEAN(values, trim_pct)` | Mean after trimming extreme percentiles | |
| 127 | +| `SOV_DESCRIBE(values)` | Descriptive statistics table (count, mean, std, min, max, …) | |
| 128 | + |
| 129 | +### Indexing & normalisation |
| 130 | + |
| 131 | +| Function | Description | |
| 132 | +|----------|-------------| |
| 133 | +| `SOV_RANK_PCT(values)` | Rank each value as a percentile | |
| 134 | +| `SOV_ZSCORE(values)` | Z-score standardisation | |
| 135 | +| `SOV_NORMALIZE_MINMAX(values)` | Min-max normalisation to [0, 1] | |
| 136 | +| `SOV_INDEX_TO_BASE(values, base_period)` | Rebase a series to a chosen base period = 100 | |
| 137 | + |
| 138 | +### Forecasting |
| 139 | + |
| 140 | +| Function | Description | |
| 141 | +|----------|-------------| |
| 142 | +| `SOV_LINEAR_FORECAST(x, y, x_new)` | OLS linear regression forecast | |
| 143 | +| `SOV_EXP_SMOOTHING(values, alpha)` | Simple exponential smoothing | |
| 144 | +| `SOV_HOLT_FORECAST(values, periods_ahead)` | Holt double-exponential smoothing forecast (α and β fitted automatically) | |
| 145 | +| `SOV_MOVING_AVG_FORECAST(values, window, periods)` | Moving-average extrapolation | |
| 146 | +| `SOV_SEASONAL_DECOMPOSE(values, period)` | Seasonal decomposition (trend + seasonal + residual) | |
| 147 | + |
| 148 | +### Modelling utilities |
| 149 | + |
| 150 | +| Function | Description | |
| 151 | +|----------|-------------| |
| 152 | +| `SOV_REGRESSION(y, x_matrix)` | OLS regression; returns coefficients and R² | |
| 153 | +| `SOV_CORRELATION_MATRIX(data_matrix)` | Pairwise correlation matrix | |
| 154 | +| `SOV_MONTE_CARLO(mean, std, n_sims, n_steps)` | Monte Carlo path simulation | |
| 155 | +| `SOV_SCENARIO_TABLE(base, shocks)` | Scenario analysis table | |
| 156 | + |
| 157 | +### Utilities |
| 158 | + |
| 159 | +| Function | Description | |
| 160 | +|----------|-------------| |
| 161 | +| `SOV_ARRAY_SHAPE(range)` | Return `{rows, cols}` of an Excel range | |
| 162 | +| `SOV_FLATTEN(range)` | Flatten a 2-D range to a single column | |
| 163 | +| `SOV_DATE_DIFF_BUS(start_date, end_date)` | Business-day count between two dates | |
| 164 | + |
| 165 | +### Fiscal sustainability |
| 166 | + |
| 167 | +| Function | Description | |
| 168 | +|----------|-------------| |
| 169 | +| `SOV_DEBT_TRAJECTORY(debt_gdp, r, g, primary_balance, years)` | Simulate debt-to-GDP path under r–g and primary balance assumptions | |
| 170 | +| `SOV_FISCAL_REACTION(debt_gdp, output_gap)` | Estimate fiscal reaction function coefficient | |
| 171 | +| `SOV_IMPLICIT_INTEREST_RATE(interest_payments, debt_stock)` | Implicit (effective) interest rate on public debt | |
| 172 | +| `SOV_DEBT_STABILIZING_PB(debt_gdp, r, g)` | Primary balance required to stabilise debt ratio | |
| 173 | + |
| 174 | +### Credit risk |
| 175 | + |
| 176 | +| Function | Description | |
| 177 | +|----------|-------------| |
| 178 | +| `SOV_MERTON_DEFAULT_PROB(asset_value, debt, volatility, r, T)` | Merton structural probability of default | |
| 179 | +| `SOV_CDS_DEFAULT_PROB(cds_spread, recovery_rate)` | Risk-neutral default probability from CDS spread | |
| 180 | +| `SOV_ZSCORE_SOVEREIGN(financials)` | Altman-style Z-score adapted for sovereign issuers | |
| 181 | +| `SOV_SPREAD_DECOMPOSITION(spread, risk_free, liquidity_premium)` | Decompose spread into credit, liquidity, and other components | |
| 182 | + |
| 183 | +### Yield curve |
| 184 | + |
| 185 | +| Function | Description | |
| 186 | +|----------|-------------| |
| 187 | +| `SOV_NELSON_SIEGEL(tenors, beta0, beta1, beta2, tau)` | Nelson-Siegel yield curve fit | |
| 188 | +| `SOV_ZSPREAD(cashflows, schedule, price, risk_free_curve)` | Z-spread to the risk-free curve | |
| 189 | +| `SOV_CARRY_ROLLDOWN(tenors, yields, holding_period)` | Carry-and-roll-down return estimate | |
| 190 | +| `SOV_ASW_SPREAD(coupon, maturity, price, swap_curve)` | Asset-swap spread calculation | |
| 191 | + |
| 192 | +### Reserves & external sector |
| 193 | + |
| 194 | +| Function | Description | |
| 195 | +|----------|-------------| |
| 196 | +| `SOV_RESERVES_ADEQUACY(reserves, imports, st_debt, m2)` | IMF ARA metric — composite reserve adequacy ratio | |
| 197 | +| `SOV_BOP_FINANCING_GAP(current_account, fdi, portfolio, amortisation)` | Balance-of-payments financing gap | |
| 198 | +| `SOV_FX_MISALIGNMENT(real_exchange_rate, fundamentals)` | REER misalignment from estimated equilibrium | |
| 199 | + |
| 200 | +### Stress testing |
| 201 | + |
| 202 | +| Function | Description | |
| 203 | +|----------|-------------| |
| 204 | +| `SOV_FAN_CHART_DEBT(baseline, shocks, periods, n_sims)` | Monte Carlo fan chart for debt trajectory | |
| 205 | +| `SOV_CONTINGENT_LIABILITY_SHOCK(debt_gdp, shock_size, gdp_impact)` | One-off shock to debt ratio from contingent liabilities | |
| 206 | +| `SOV_FX_PASSTHROUGH_DEBT(fx_debt_share, depreciation, debt_gdp)` | Debt ratio impact of exchange-rate depreciation | |
| 207 | + |
| 208 | +### Amortisation & financing |
| 209 | + |
| 210 | +| Function | Description | |
| 211 | +|----------|-------------| |
| 212 | +| `SOV_AMORTIZATION_PROFILE(bonds_list)` | Amortisation schedule from a list of bond maturities and amounts | |
| 213 | +| `SOV_WEIGHTED_AVG_MATURITY(bonds_outstanding)` | Weighted average maturity of debt portfolio | |
| 214 | +| `SOV_GROSS_FINANCING_NEED(deficit, amortisation)` | Annual gross financing need | |
| 215 | + |
| 216 | +### Political risk & ESG |
| 217 | + |
| 218 | +| Function | Description | |
| 219 | +|----------|-------------| |
| 220 | +| `SOV_POLITICAL_RISK_SCORE(indicators)` | Composite political-risk score | |
| 221 | +| `SOV_ESG_SOVEREIGN_SCORE(e, s, g, weights)` | Weighted ESG score for a sovereign | |
| 222 | +| `SOV_SANCTIONS_EXPOSURE(trade_matrix, sanctioned_countries)` | Bilateral trade-weighted sanctions-exposure index | |
| 223 | + |
| 224 | +### Contagion |
| 225 | + |
| 226 | +| Function | Description | |
| 227 | +|----------|-------------| |
| 228 | +| `SOV_CONTAGION_BETA(target_spreads, peer_spreads)` | OLS contagion beta of target vs peer spreads | |
| 229 | +| `SOV_DCC_GARCH_CORR(series_a, series_b)` | DCC-GARCH dynamic conditional correlation | |
| 230 | +| `SOV_GRANGER_CAUSALITY(y, x, max_lags)` | Granger-causality p-value between spread series | |
| 231 | +| `SOV_TRADE_LINKAGE_MATRIX(trade_flows)` | Normalised bilateral trade-linkage matrix | |
| 232 | + |
| 233 | +### Debt composition |
| 234 | + |
| 235 | +| Function | Description | |
| 236 | +|----------|-------------| |
| 237 | +| `SOV_ORIGINAL_SIN_INDEX(fx_debt, local_debt)` | Original-sin index (share of FX-denominated debt) | |
| 238 | +| `SOV_HIDDEN_DEBT_ESTIMATOR(reported, off_balance)` | Estimate of unreported / hidden debt obligations | |
| 239 | +| `SOV_DEBT_TRANSPARENCY_SCORE(disclosure_flags)` | Debt-transparency score from creditor-disclosure indicators | |
| 240 | +| `SOV_COLLATERALIZED_DEBT_FLAG(collateral_value, debt_amount)` | Flag bonds with collateralised structures | |
| 241 | + |
| 242 | +### Macro-financial linkages |
| 243 | + |
| 244 | +| Function | Description | |
| 245 | +|----------|-------------| |
| 246 | +| `SOV_BANK_NEXUS_SCORE(bank_holdings, total_assets)` | Sovereign-bank nexus concentration score | |
| 247 | +| `SOV_MONETARY_FINANCING_RISK(central_bank_claims, gdp)` | Monetary-financing-risk ratio | |
| 248 | +| `SOV_RG_DIFFERENTIAL(real_rate, real_growth)` | r–g differential driving debt dynamics | |
| 249 | +| `SOV_DOLLARIZATION_VULNERABILITY(fx_liabilities, total_liabilities)` | Dollarisation vulnerability index | |
| 250 | + |
| 251 | +### Market microstructure |
| 252 | + |
| 253 | +| Function | Description | |
| 254 | +|----------|-------------| |
| 255 | +| `SOV_BID_ASK_LIQUIDITY_SCORE(bid, ask, mid)` | Normalised bid-ask liquidity score | |
| 256 | +| `SOV_LOCAL_VS_EXTERNAL_BASIS(local_yield, external_yield, fx_forward)` | Local-vs-external basis after FX hedging | |
| 257 | +| `SOV_AUCTION_TAIL_ANALYSIS(bids, allotment)` | Auction tail (yield spread between average and cut-off) | |
| 258 | +| `SOV_INVESTOR_BASE_CONCENTRATION(holdings_by_type)` | Herfindahl–Hirschman investor-base concentration index | |
| 259 | + |
| 260 | +### IMF framework |
| 261 | + |
| 262 | +| Function | Description | |
| 263 | +|----------|-------------| |
| 264 | +| `SOV_DSA_REPLICATION(debt_gdp, r, g, pb_path)` | Replicate IMF debt sustainability analysis (DSA) projection | |
| 265 | +| `SOV_IMF_PROGRAM_PROBABILITY(macro_indicators)` | Probability of an IMF programme request | |
| 266 | +| `SOV_EXCEPTIONAL_ACCESS_CHECK(debt_gdp, financing_gap, market_access)` | IMF exceptional-access criteria checklist | |
| 267 | +| `SOV_SDRS_ALLOCATION_IMPACT(quota_share, sdr_allocation)` | Impact of SDR allocation on reserve adequacy | |
| 268 | + |
| 269 | +### Event studies |
| 270 | + |
| 271 | +| Function | Description | |
| 272 | +|----------|-------------| |
| 273 | +| `SOV_RESTRUCTURING_COMPARABLES(debt_gdp, gdp_growth, comparables)` | Comparable restructuring case analysis | |
| 274 | +| `SOV_EVENT_STUDY_SPREAD(spreads, event_date, window)` | Abnormal spread reaction around an event date | |
| 275 | +| `SOV_CRISIS_EARLY_WARNING(macro_panel)` | Early-warning signal from macro vulnerability indicators | |
| 276 | + |
| 277 | +### Plotting (inline charts) |
| 278 | + |
| 279 | +These functions render a Matplotlib chart to PNG and return it as a **PyXLL inline image** — the chart appears directly inside the Excel cell that holds the formula. Outputs are LRU-cached (up to 128 entries) so recalculation is fast. |
| 280 | + |
| 281 | +| Function | Description | |
| 282 | +|----------|-------------| |
| 283 | +| `SDXL_PLOT_YIELD_CURVE(tenors, yields, [title], [x_label], [y_label], [width_px], [height_px], [style])` | Plot a yield curve; `style` is `"line"` (default) or `"markers"` | |
| 284 | +| `SDXL_PLOT_TIMESERIES(dates, values, [title], [width_px], [height_px])` | Plot a time series; dates accept Excel serials, ISO strings, or date objects | |
| 285 | +| `SDXL_PLOT_ROLLING_AVG(dates, values, window, [title], [width_px], [height_px])` | Plot raw data with a rolling-average overlay; `window` is the number of periods (default 20) | |
| 286 | + |
| 287 | +**Example formulas:** |
| 288 | + |
| 289 | +```excel |
| 290 | +=SDXL_PLOT_YIELD_CURVE(A2:A10, B2:B10, "UST Curve") |
| 291 | +=SDXL_PLOT_TIMESERIES(A2:A300, B2:B300, "10yr UST Yield") |
| 292 | +=SDXL_PLOT_ROLLING_AVG(A2:A300, B2:B300, 20, "Rolling Avg (20d)") |
| 293 | +``` |
| 294 | + |
| 295 | +Error conditions (mismatched lengths, empty input) return an `#SDXL:` prefixed string so Excel can display a readable error in the cell. |
| 296 | + |
| 297 | +--- |
| 298 | + |
| 299 | +## Running tests |
| 300 | + |
| 301 | +The test suite uses [pytest](https://pytest.org/) and mocks the `pyxll` runtime via `conftest.py`, so no Excel or PyXLL installation is required. |
| 302 | + |
| 303 | +```bash |
| 304 | +# Install dependencies and the package |
| 305 | +pip install -r requirements.txt |
| 306 | +pip install -e ".[dev]" |
| 307 | + |
| 308 | +# Run all tests from the repo root |
| 309 | +python -m pytest |
| 310 | +``` |
| 311 | + |
| 312 | +Test files mirror the module they cover (`test_averaging.py` → `averaging.py`, `test_plots.py` → `plots.py`, etc.) and live at the repo root alongside `conftest.py`. |
| 313 | + |
0 commit comments