Skip to main content
Glama
eqlx-portfolio

eqlx-portfolio

Official
by eqlx-ai

eqlx-portfolio

Deterministic credit portfolio risk: how much a loan book can lose in a bad year, how much capital that takes, and which borrowers and sectors drive it. And what a new loan has to earn to be worth that capital. A Python library, a command line tool and a read-only MCP server for AI agents.

Loans are assessed one by one, but they fail together: a hundred loans to a hundred industries and a hundred loans to one industry look the same loan by loan and are very different portfolios. This tool measures that difference.

It computes. It does not estimate default probabilities, recoveries or correlations: those are inputs. Same input, same output, and every figure comes with the steps that produced it.

  • Two methods, one input. A seeded simulation of the asset value model (the approach of CreditMetrics) and the closed-form CreditRisk+ model. Use one to question the other.

  • Checked against published numbers. The worked examples of the CreditMetrics Technical Document and of the CreditRisk+ document are reproduced in the test-suite; everything else is checked against closed forms, direct enumeration and brute-force simulation.

  • Built for agents. The agent decides what to look at; the tool returns the number, the band around it and the steps taken. The MCP server only reads files in one folder and writes nothing.

Who it is for

Credit risk and capital planning teams who want an independent view of economic capital and concentration, credit committees asking what one more loan does to the book, validators who need a transparent benchmark for a vendor model, and people building AI agents for credit work.

Related MCP server: Sablier MCP Server

Install

pip install eqlx-portfolio            # library and command line
pip install "eqlx-portfolio[mcp]"     # with the MCP server

Python 3.10 or newer. Depends on NumPy and SciPy only.

A first look

eqlx-portfolio sample demo            # writes a made-up portfolio: demo/loans.csv, demo/settings.json
eqlx-portfolio run demo/loans.csv --config demo/settings.json
eqlx-portfolio 0.2.0  method: simulation
loans 621  borrowers 500  sectors 6  exposure 995,349,000

expected loss                12,716,172
loss std deviation           12,031,533
at 99.00% confidence
  value-at-risk              57,233,350   95% band 56,635,700 to 57,801,100
  expected shortfall         71,379,023
  economic capital           44,517,178
at 99.90% confidence
  value-at-risk              91,343,950   95% band 88,673,050 to 93,627,200
  expected shortfall        104,348,278
  economic capital           78,627,778

largest borrower 5.5% of exposure, ten largest 28.0%, effective number of borrowers 79

sector                exposure share  risk share
construction                   41.1%       69.1%
services                       19.4%       13.0%
manufacturing                  14.8%        6.8%
retail                         11.7%        4.7%
tourism                         5.6%        3.5%
energy                          7.4%        2.9%

borrower              exposure share  risk share
B0004                           4.0%       12.7%
B0003                           4.7%       10.6%
B0005                           4.2%        8.8%
B0001                           5.5%        7.4%
B0002                           2.4%        3.8%
B0287                           1.8%        3.6%
B0303                           1.1%        2.7%
B0136                           0.7%        2.6%
B0407                           1.0%        2.1%
B0435                           0.5%        2.0%

Construction is 41% of the exposure and 69% of the risk: five large borrowers were planted there. The band next to each value-at-risk says how far simulation noise can move it. The same run with --json adds the checks that were made and the steps taken.

What does one more loan do? Fifty million to a new construction borrower, then the same loan to an energy borrower:

eqlx-portfolio what-if demo/loans.csv --config demo/settings.json --new new_loan.csv

New loan of 50,000,000 (pd 2%, lgd 45%)

Expected loss

Expected shortfall at 99.9%

Capital per unit lent

to construction

+450,000

+8,530,008

17.1%

to energy

+450,000

+3,824,232

7.6%

Same loan, same expected loss, twice the capital: it lands where the book is already heavy. Both portfolios are valued on the same scenarios, so the difference is not simulation noise.

What does that loan have to earn? Say it runs three years with quarterly interest, funding costs 40%, the bank wants 45% on its capital after 30% tax, the capital itself earns 38%, operating costs are 1%, and 43% is on offer:

eqlx-portfolio price demo/loans.csv --config demo/settings.json --new new_loan.csv --terms terms.json

The same loan, offered at 43%

Capital tied up

Hurdle rate

RAROC

Earns the target?

to construction, capital from this book

16.2%

43.76%

41.7%

no

to energy, capital from this book

6.7%

42.82%

46.9%

yes

either, capital from the Basel formula

7.7%

42.91%

45.8%

yes

either, capital from a 100% risk weight at 12%

12.0%

43.34%

43.0%

no

The hurdle rate is funding cost (40.00%) + expected loss (1.11%) + operating cost (1.03%) + the cost of the capital tied up. Only the last part differs, and only the book itself can tell the two borrowers apart: a formula or a risk weight prices them the same.

Other commands: contributions (every borrower's share of the risk, as CSV), describe (what a loan file contains), pair (joint default probability of two borrowers).

As a library

import numpy as np
from eqlx_portfolio import (
    creditriskplus,
    default_thresholds,
    joint_default_probability,
    large_portfolio_loss,
    migration_thresholds,
    sigma_from_asset_correlation,
    simulate,
    single_factor,
    value_stats,
)

# One bond, rated BBB: where can it be in a year, and what is it worth there?
bbb = [0.0002, 0.0033, 0.0595, 0.8693, 0.0530, 0.0117, 0.0012, 0.0018]  # AAA ... CCC, default
values = [109.37, 109.19, 108.66, 107.55, 102.02, 98.10, 83.64, 51.13]
s = value_stats(bbb, values)
round(s.mean, 2), round(s.sd, 2)  # (107.09, 2.99)
np.round(migration_thresholds(bbb), 2)  # [ 3.54  2.7   1.53 -1.49 -2.18 -2.75 -2.91]

# Two borrowers with 0.18% and 0.06% default probability and 30% asset correlation
joint_default_probability(0.0018, 0.0006, 0.30)[0]  # 1.561e-05, 14.5 times the independent 1.08e-06

# 100 loans of 1,000,000, pd 2%, lgd 45%, one common factor with 20% asset correlation
pd = np.full(100, 0.02)
amount = np.full(100, 1_000_000 * 0.45)
sim = simulate(
    single_factor(0.20, 100),
    default_thresholds(pd),
    np.column_stack((np.zeros(100), amount)),
    scenarios=200_000,
    seed=1,
    tail_levels=[0.999],
)
tail = sim.tails[0]
round(tail.value_at_risk), round(tail.expected_shortfall)  # (10800000, 13137313)
round(large_portfolio_loss(amount, pd, 0.20, 0.999))  # 10184076: the infinitely fine limit

# The same portfolio in CreditRisk+, with the sector volatility that matches the 20% correlation
sigma = sigma_from_asset_correlation(0.02, 0.20)  # 1.323
res = creditriskplus(amount, pd, np.ones((100, 1)), [sigma], confidence=[0.999])
round(res.tails[0].value_at_risk), round(res.tails[0].expected_shortfall)  # (9900000, 11375006)

For AI agents (MCP)

eqlx-portfolio mcp --root /path/to/folder

Client configuration (Claude Desktop, Claude Code and other MCP clients):

{"mcpServers": {"eqlx-portfolio": {"command": "eqlx-portfolio", "args": ["mcp", "--root", "/path/to/folder"]}}}

Tool

What it returns

list_files

loan and settings files in the folder

describe_loans

loans, borrowers, sectors, exposure, expected loss, largest borrowers

portfolio_risk

expected loss, value-at-risk, expected shortfall, economic capital, concentration, steps

risk_contributions

borrowers ranked by their share of the risk, next to their share of exposure

what_if_new_loans

the portfolio before and after new loans, on the same scenarios

price_new_loan

hurdle rate and RAROC of a new loan, with capital measured from the book, the Basel formula and a risk weight

loan_price

cost components, hurdle rate and RAROC when the capital is already known

joint_default

probability that two borrowers default together

joint_migration

joint year-end rating probabilities of two borrowers

exposure_value

mean and standard deviation of one exposure's value

large_portfolio_benchmark

loss rate of a perfectly fine-grained portfolio in a bad year

The server reads .csv and .json files inside --root and nothing else; .. and symlinks cannot leave the folder. It writes nothing and sends nothing anywhere. When an input is refused the agent gets the reason, not a bare error.

Input

Loan file (CSV), one row per loan:

Column

Meaning

loan_id

unique

exposure

amount at risk if the borrower defaults

pd

default probability over the horizon, at least 0 and below 1

lgd

share of the exposure lost in default, 0 to 1

borrower_id

optional; loans of one borrower default together

sector

optional; needed for the sector correlation model

lgd_sd

optional; makes the loss in default uncertain (simulation only)

Settings (JSON). The correlation block is required: it is the main assumption of a run.

{
  "method": "simulation",
  "confidence": [0.99, 0.999],
  "scenarios": 200000,
  "seed": 1,
  "correlation": {
    "model": "sector",
    "within": {"construction": 0.24, "energy": 0.18},
    "default_within": 0.15,
    "default_between": 0.5,
    "between": {"construction|energy": 0.3}
  }
}

Correlation model

Meaning

single

one common factor; rho is the asset correlation of any two borrowers

basel_corporate

one common factor; each borrower's correlation from the Basel corporate formula (12% to 24% by pd)

sector

one factor per sector; within a sector, and between the sector factors

"method": "creditriskplus" solves the portfolio in closed form instead. It accepts the single and sector models with independent sectors; the sector volatility is derived from the asset correlation unless given in "creditriskplus": {"sector_sigma": {...}}.

How it is verified

Published figures, from the CreditMetrics Technical Document (J.P. Morgan, 1997):

Example

Published

Reproduced

Value of a BBB bond after one year: mean, standard deviation (Table 2.5)

107.09, 2.99

107.09, 2.99

The same with uncertain recovery

3.18

3.18

Revaluation of the bond in each rating from the forward curves (Tables 1.2, 1.4)

16 values

within 0.02

Asset return thresholds of BB and single-A borrowers (Tables 8.5, 8.6)

14 values

all 14

Joint migration of two borrowers, 0.30 asset correlation (Table 3.2)

64 cells

within 0.01 point

Joint migration of two borrowers, 0.20 asset correlation (Table 8.7)

64 cells

within 0.05 point

Scenarios of asset returns mapped to ratings (Tables 10.4, 10.5)

30 ratings

all 30

First percentile of a two-bond portfolio

204.40

204.40

Three-bond portfolio: means and variances (chapter 9)

7 values

all 7

Published figures, from the CreditRisk+ document (Credit Suisse First Boston, 1997), example 1A, 25 obligors in one sector:

Figure

Published

Reproduced

Aggregate exposure, expected loss

130,513,072 and 14,221,863

the same

Standard deviation

12,668,742

12,613,315 (0.4% lower)

99th percentile loss

55,311,503

55,240,000 (0.1% lower)

99.9th percentile loss

77,133,478

76,990,000 (0.2% lower)

The document's spreadsheet groups exposures into bands by a rounding rule it does not spell out; the small gaps are that rounding.

Three figures printed in the CreditMetrics document do not follow from its own tables (a misprinted probability, a misprinted threshold, and covariances that the model cannot produce). The tests show where each comes from; see LIMITS.md.

Independent truth for everything else:

  • the bivariate normal distribution against SciPy, to 12 decimals;

  • simulated mean and standard deviation against exact pairwise formulas;

  • a fine-grained portfolio against the large-portfolio formula behind the Basel risk weights;

  • CreditRisk+ against the negative binomial and Poisson closed forms (to 14 decimals) and against a brute-force simulation of the same model, including every borrower's risk contribution;

  • risk contributions add up to the expected shortfall, in both methods;

  • a what-if run reproduces, to the last digit, the separate run of the portfolio without the new loans, also when the new loan brings a new sector or an uncertain loss given default;

  • loan pricing against the closed form of the scheme it follows (Engelmann and Pham 2020, eq. 28), a one-year loan worked by hand, and the identity that RAROC at the hurdle rate is the target;

  • every defect found by an independent review is pinned by a regression test.

188 tests. uv run pytest runs them in two to three minutes.

Limits

One period, default mode in portfolio runs, inputs are yours, the far tail of a simulation is thin, CreditRisk+ sectors are independent. The hurdle rate is a break-even point, not a recommended price, and loan pricing is verified by formula, not by published tables. No estimation of correlations, no granularity adjustment, no stress scenarios, not regulatory capital. Details in LIMITS.md.

References

  • Gupton, G. M., Finger, C. C., Bhatia, M. (1997). CreditMetrics - Technical Document. J.P. Morgan.

  • Credit Suisse Financial Products (1997). CreditRisk+: A Credit Risk Management Framework.

  • Vasicek, O. (2002). The distribution of loan portfolio value. Risk, December.

  • Gordy, M. B. (2000). A comparative anatomy of credit risk models. Journal of Banking & Finance 24.

  • Gordy, M. B. (2003). A risk-factor model foundation for ratings-based bank capital rules. Journal of Financial Intermediation 12.

  • Engelmann, B., Pham, H. (2020). A RAROC valuation scheme for loans and its application in loan origination. Risks 8(2), 63.

  • European Banking Authority (2020). Guidelines on loan origination and monitoring (EBA/GL/2020/06), section 6.

  • Haaf, H., Tasche, D. (2002). Calculating value-at-risk contributions in CreditRisk+. GARP Risk Review 7.

  • Haaf, H., Reiss, O., Schoenmakers, J. (2004). Numerically stable computation of CreditRisk+. In CreditRisk+ in the Banking Industry, Springer.

  • Basel Committee on Banking Supervision (2005). An Explanatory Note on the Basel II IRB Risk Weight Functions.

  • Drezner, Z., Wesolowsky, G. O. (1990). On the computation of the bivariate normal integral. Journal of Statistical Computation and Simulation 35.

License

Apache-2.0. See LICENSE and NOTICE.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      B
      quality
      D
      maintenance
      An MCP server that lets AI assistants read and visually analyze local documents — PDFs, Excel spreadsheets, CSV files, Word documents, PowerPoint presentations, and images.
      4
      57 npm
      75 PyPI
      MIT
    • F
      license
      Not graded
      quality
      D
      maintenance
      MCP server that exposes the credit scoring model's deterministic tools (probability of default, SHAP explanations, typicality check, financial ratios) to AI agents, enabling natural language credit risk assessment.
      -
    • A
      license
      Not graded
      quality
      D
      maintenance
      MCP server for quantitative portfolio optimization and analysis, providing tools for market data ingestion, covariance estimation, portfolio construction (mean-variance, Black-Litterman, HRP, CVaR, regularized), risk attribution, and statistical analysis to AI assistants.
      MIT