# Private credit tape (private_credit_tape)

Desk: Credit. Page: https://hedgefund.md/models/private_credit_tape

**What it asks.** How do this loan tape's losses and interest fall through its tranches, and how much sits under the senior at a 1% chance of impairment?

Runs a loan tape through a one-factor Gaussian copula and a senior-first waterfall. Gives each tranche's expected loss, impairment chance, 99% tail loss and coupon cover, the pool loss distribution, and the junior size that puts the senior's impairment chance at 1%. Loans, rates and default rates come from the caller. Not a rating, an offering or advice.

Results are hypothetical: arithmetic on the numbers sent, a made-up template or a stated default. Nobody achieved them, and they aren't a forecast, a recommendation, an offer or a record of real trading. Don't show any output to investors or clients, or use it to market an investment. Terms: https://hedgefund.md/terms

## Inputs

| Input | Type | Range | Required | What it is |
| --- | --- | --- | --- | --- |
| `loans` | array | 1 to 100 items | yes | The loan tape. Up to 100 loans. |
| `loans[].name` | string |  | yes | A label, such as Loan 3. |
| `loans[].balance` | number | 0.01 to 10000000000000 | yes | Loan balance. |
| `loans[].rate_pct` | number | 0 to 100 | yes | Interest rate a year. 10 means 10%. |
| `loans[].pd_annual_pct` | number | 0 to 100 | yes | Yearly default chance. 3 means 3%. |
| `loans[].lgd_pct` | number | 0 to 100 | yes | Loss given default, as a share of balance. 45 means 45%. |
| `tranches` | array | 1 to 10 items | yes | Tranches, senior first, sizes summing to 100. |
| `tranches[].name` | string |  | yes | A label, such as Senior. |
| `tranches[].size_pct` | number | 0.01 to 100 | yes | Share of the pool. 70 means 70%. |
| `tranches[].coupon_pct` | number | 0 to 100 | yes | Coupon a year on the tranche's starting size. 0 for a residual. |
| `asset_correlation_pct` | number | 0 to 100 | yes | Asset correlation rho in the one-factor copula. 20 means 20%. |
| `horizon_years` | number | 0.25 to 30 | yes | Years the tape runs. |
| `servicing_bps` | number | 0 to 1000 | no | Servicing fee a year on the starting pool, paid first from interest. Default 0. |
| `sims` | whole number | 100 to 5000 | no | Monte Carlo paths, up to 5000. Default 5000. |
| `seed` | whole number | 0 to 2147483647 | no | Seed for the draws. The same seed gives the same bytes. Default 1. |

## Template

Made-up numbers that run as they stand, so you can see the shape of a result. They aren't market levels, typical values or estimates, and the names in them don't exist. Replace every number before you read anything into a run.

```json
{
  "loans": [
    {
      "name": "Loan 1",
      "balance": 20000000,
      "rate_pct": 9,
      "pd_annual_pct": 1.5,
      "lgd_pct": 40
    },
    {
      "name": "Loan 2",
      "balance": 12000000,
      "rate_pct": 10,
      "pd_annual_pct": 2,
      "lgd_pct": 45
    },
    {
      "name": "Loan 3",
      "balance": 10000000,
      "rate_pct": 10,
      "pd_annual_pct": 2,
      "lgd_pct": 45
    },
    {
      "name": "Loan 4",
      "balance": 10000000,
      "rate_pct": 11,
      "pd_annual_pct": 3,
      "lgd_pct": 50
    },
    {
      "name": "Loan 5",
      "balance": 8000000,
      "rate_pct": 11,
      "pd_annual_pct": 3,
      "lgd_pct": 50
    },
    {
      "name": "Loan 6",
      "balance": 8000000,
      "rate_pct": 12,
      "pd_annual_pct": 4,
      "lgd_pct": 55
    },
    {
      "name": "Loan 7",
      "balance": 8000000,
      "rate_pct": 12,
      "pd_annual_pct": 4,
      "lgd_pct": 55
    },
    {
      "name": "Loan 8",
      "balance": 6000000,
      "rate_pct": 13,
      "pd_annual_pct": 5,
      "lgd_pct": 60
    },
    {
      "name": "Loan 9",
      "balance": 6000000,
      "rate_pct": 13,
      "pd_annual_pct": 5,
      "lgd_pct": 60
    },
    {
      "name": "Loan 10",
      "balance": 5000000,
      "rate_pct": 14,
      "pd_annual_pct": 6,
      "lgd_pct": 60
    },
    {
      "name": "Loan 11",
      "balance": 4000000,
      "rate_pct": 15,
      "pd_annual_pct": 8,
      "lgd_pct": 65
    },
    {
      "name": "Loan 12",
      "balance": 3000000,
      "rate_pct": 16,
      "pd_annual_pct": 10,
      "lgd_pct": 70
    }
  ],
  "tranches": [
    { "name": "Senior", "size_pct": 75, "coupon_pct": 6.5 },
    { "name": "Mezzanine", "size_pct": 15, "coupon_pct": 10 },
    { "name": "Junior", "size_pct": 10, "coupon_pct": 12 }
  ],
  "asset_correlation_pct": 20,
  "horizon_years": 3,
  "servicing_bps": 50,
  "sims": 5000,
  "seed": 7
}
```

## What the template gives

On these numbers, across 5000 hypothetical paths with seed 7 over 3 years, the pool loses 5.42% on average and 25.8% at the 99th percentile, and the senior, with 25% under it, is impaired on 1.2% of paths. A size of 25.8% under it would put that chance at 0.98%.

- Pool expected loss: 5.4195% of pool
- Monte Carlo error on that: 0.0842347% of pool
- Pool loss stdev: 5.9563% of pool
- Pool loss, 95th percentile: 17.4% of pool
- Pool loss, 99th percentile: 25.8% of pool
- Size under the senior for a 1% chance: 25.8% of pool (breach)
- Senior impairment chance: 1.2% (breach)
- Mean excess interest: 7.6876% of pool

Where it flips, with the other inputs held:

- `junior_size_pct` at 25.8: With less than this much under the senior, more than 1% of these paths impair it.
- `asset_correlation_pct` at 16.81566238: Above this correlation, more than 1% of these same paths impair the senior.
- `default_rate_multiple` at 0.891404079: Above this multiple of every loan's default rate, more than 1% of these same paths impair the senior.

## Formula

Each path draws a market factor Z and one e per loan. A loan defaults within the horizon if sqrt(rho) Z + sqrt(1 - rho) e < normInv(1 - (1 - pd)^H). Pool loss is the sum of balance * LGD over defaults. Each tranche takes min(max(loss - attach, 0), size). Interest pays servicing, then coupons senior first.

## Assumptions

- Draw order on each path is Z, then one e per loan in list order, from one stream seeded by the seed and the model's id. Correlation and default rates change thresholds, not draws, so moving them keeps the same paths.
- Interest is simple. A performing loan pays balance * rate * H. A defaulted loan pays for half the horizon, as if it defaulted at the midpoint.
- Servicing is charged on the starting pool for the whole horizon and is paid first from interest.
- Coupons are due on each tranche's starting size for the whole horizon, even after a write-down. Interest and principal run separately: excess interest is reported and doesn't absorb losses.
- Losses hit the most junior tranche first and are taken in full at the horizon. A loss landing exactly on an attachment point impairs nothing above it. A tranche's loss figures are percents of its own size, and pool figures are percents of the pool.
- Coupon cover for a tranche is mean interest after servicing, over the coupons due on that tranche and everything senior to it.
- Percentiles use linear interpolation between order statistics, except the junior size. That one is an order statistic: the smallest size with at most 1% of these paths above it, recounted from the same draws, and rounded up at the tenth digit so it still passes when sent back.
- Correlation moves losses between tranches, but it leaves the pool's expected loss alone. The exact figure is the sum of balance * LGD * (1 - (1 - pd)^H) over the pool.
- The sleeve is for the whole pool: gross and net are 100, tail is the 99th percentile pool loss, and carry is mean interest after servicing, per year.

## Not modeled

- Prepayment, amortization and new loans. Each balance stays flat until the horizon.
- Recovery timing. A loss is taken in full at the horizon, with no workout period.
- Coverage triggers, cash traps and any turbo that would redirect excess interest to principal.
- Default timing beyond the midpoint convention, and correlation that changes in stress.
- Fat tails: the copula is Gaussian, so joint extreme defaults are thin compared with a heavier-tailed one.
- Rate changes on floating loans, hedges, reserve accounts and fees other than servicing.
- Secondary-market prices, liquidity and, for tokenized notes, anything about the chain, the wrapper or transfer limits.
- Taxes.

## Call it

Every result has the same fields: summary, readings, tables, charts, flags, flips, formula, assumptions, not_modeled, hypothetical and inputs_hash. Read flags, flips and not_modeled with the summary, and pass them along together.

### HTTP

No key. The tools keep nothing you send. Send positions by POST, because a URL can land in a server log.

```bash
curl -s https://hedgefund.md/api/v1/private_credit_tape \
  -H 'content-type: application/json' \
  -d '{"loans":[{"name":"Loan 1","balance":20000000,"rate_pct":9,"pd_annual_pct":1.5,"lgd_pct":40},{"name":"Loan 2","balance":12000000,"rate_pct":10,"pd_annual_pct":2,"lgd_pct":45},{"name":"Loan 3","balance":10000000,"rate_pct":10,"pd_annual_pct":2,"lgd_pct":45},{"name":"Loan 4","balance":10000000,"rate_pct":11,"pd_annual_pct":3,"lgd_pct":50},{"name":"Loan 5","balance":8000000,"rate_pct":11,"pd_annual_pct":3,"lgd_pct":50},{"name":"Loan 6","balance":8000000,"rate_pct":12,"pd_annual_pct":4,"lgd_pct":55},{"name":"Loan 7","balance":8000000,"rate_pct":12,"pd_annual_pct":4,"lgd_pct":55},{"name":"Loan 8","balance":6000000,"rate_pct":13,"pd_annual_pct":5,"lgd_pct":60},{"name":"Loan 9","balance":6000000,"rate_pct":13,"pd_annual_pct":5,"lgd_pct":60},{"name":"Loan 10","balance":5000000,"rate_pct":14,"pd_annual_pct":6,"lgd_pct":60},{"name":"Loan 11","balance":4000000,"rate_pct":15,"pd_annual_pct":8,"lgd_pct":65},{"name":"Loan 12","balance":3000000,"rate_pct":16,"pd_annual_pct":10,"lgd_pct":70}],"tranches":[{"name":"Senior","size_pct":75,"coupon_pct":6.5},{"name":"Mezzanine","size_pct":15,"coupon_pct":10},{"name":"Junior","size_pct":10,"coupon_pct":12}],"asset_correlation_pct":20,"horizon_years":3,"servicing_bps":50,"sims":5000,"seed":7}' \
  | jq '{summary, flags, flips, not_modeled, inputs_hash}'
```

### MCP

The tool is `private_credit_tape` on the server at https://hedgefund.md/mcp.

```bash
curl -s https://hedgefund.md/mcp \
  -H 'content-type: application/json' -H 'accept: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"private_credit_tape","arguments":{"loans":[{"name":"Loan 1","balance":20000000,"rate_pct":9,"pd_annual_pct":1.5,"lgd_pct":40},{"name":"Loan 2","balance":12000000,"rate_pct":10,"pd_annual_pct":2,"lgd_pct":45},{"name":"Loan 3","balance":10000000,"rate_pct":10,"pd_annual_pct":2,"lgd_pct":45},{"name":"Loan 4","balance":10000000,"rate_pct":11,"pd_annual_pct":3,"lgd_pct":50},{"name":"Loan 5","balance":8000000,"rate_pct":11,"pd_annual_pct":3,"lgd_pct":50},{"name":"Loan 6","balance":8000000,"rate_pct":12,"pd_annual_pct":4,"lgd_pct":55},{"name":"Loan 7","balance":8000000,"rate_pct":12,"pd_annual_pct":4,"lgd_pct":55},{"name":"Loan 8","balance":6000000,"rate_pct":13,"pd_annual_pct":5,"lgd_pct":60},{"name":"Loan 9","balance":6000000,"rate_pct":13,"pd_annual_pct":5,"lgd_pct":60},{"name":"Loan 10","balance":5000000,"rate_pct":14,"pd_annual_pct":6,"lgd_pct":60},{"name":"Loan 11","balance":4000000,"rate_pct":15,"pd_annual_pct":8,"lgd_pct":65},{"name":"Loan 12","balance":3000000,"rate_pct":16,"pd_annual_pct":10,"lgd_pct":70}],"tranches":[{"name":"Senior","size_pct":75,"coupon_pct":6.5},{"name":"Mezzanine","size_pct":15,"coupon_pct":10},{"name":"Junior","size_pct":10,"coupon_pct":12}],"asset_correlation_pct":20,"horizon_years":3,"servicing_bps":50,"sims":5000,"seed":7}}}' \
  | jq '.result.structuredContent | {summary, flags, flips, not_modeled, inputs_hash}'
```

### The library

The same code on your own machine, with no dependencies, and nothing is sent anywhere.

```bash
curl -fsSL https://hedgefund.md/hedgefund.mjs -o hedgefund.mjs   # read it before you run it
node hedgefund.mjs template private_credit_tape > mine.json           # replace every number in it
node hedgefund.mjs run private_credit_tape mine.json | jq '{summary, flags, flips, not_modeled, inputs_hash}'
```

In a script:

```js
import { run, template } from "./hedgefund.mjs";

const inputs = template("private_credit_tape"); // replace every number in it
const { summary, flags, flips, not_modeled } = run("private_credit_tape", inputs);
```
