# Rates ladder (rates_ladder)

Desk: Macro. Page: https://hedgefund.md/models/rates_ladder

**What it asks.** What would this bond ladder earn over the horizon on these yields, what does a parallel shift do to it, and what shift takes the return to zero?

Prices a ladder of fixed coupon bonds from the yields sent: dirty and clean price, Macaulay and modified duration, convexity, DV01, a shock table of full repricing against the duration and convexity estimate, carry and roll-down over a horizon, and the parallel shift that takes the horizon return to zero. Yields come from the caller. It is arithmetic on those numbers, not a view on rates.

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 |
| --- | --- | --- | --- | --- |
| `bonds` | array | 1 to 40 items | yes | The ladder, up to 40 fixed coupon bonds. |
| `bonds[].name` | string |  | yes | A label for the bond, such as Bond 5y. |
| `bonds[].face` | number | 0.01 to 1000000000000 | yes | Face amount held. |
| `bonds[].coupon_pct` | number | 0 to 100 | yes | Annual coupon on face, in percent. |
| `bonds[].maturity_years` | number | 0.01 to 50 | yes | Years to maturity. Flows are counted back from it, so a fraction makes a stub. |
| `bonds[].yield_pct` | number | -5 to 60 | yes | Yield to maturity, compounded freq times a year, in percent. |
| `bonds[].freq` | whole number | 1 to 4 | yes | Coupons a year: 1, 2 or 4. |
| `shocks_bps` | array | 0 to 15 items | no | Parallel yield shocks to reprice, in basis points. Default -100, -50, 50, 100. |
| `horizon_years` | number | 0.01 to 30 | yes | Years to carry the ladder forward. |
| `curve` | array | 1 to 30 items | no | Optional curve for roll-down, up to 30 points. |
| `curve[].years` | number | 0.01 to 100 | yes | Point on the curve, in years. |
| `curve[].yield_pct` | number | -5 to 60 | yes | Yield at that point, in percent. |

## 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
{
  "bonds": [
    {
      "name": "Bond 1.25y",
      "face": 1000000,
      "coupon_pct": 3.5,
      "maturity_years": 1.25,
      "yield_pct": 3.9,
      "freq": 2
    },
    {
      "name": "Bond 2.5y",
      "face": 1000000,
      "coupon_pct": 4,
      "maturity_years": 2.5,
      "yield_pct": 4.1,
      "freq": 2
    },
    {
      "name": "Bond 3.75y",
      "face": 1000000,
      "coupon_pct": 4,
      "maturity_years": 3.75,
      "yield_pct": 4.3,
      "freq": 4
    },
    {
      "name": "Bond 5y",
      "face": 1000000,
      "coupon_pct": 4.5,
      "maturity_years": 5,
      "yield_pct": 4.5,
      "freq": 1
    },
    {
      "name": "Bond 10y",
      "face": 1000000,
      "coupon_pct": 5,
      "maturity_years": 10,
      "yield_pct": 5.1,
      "freq": 2
    }
  ],
  "shocks_bps": [-100, -50, 50, 100, 200],
  "horizon_years": 0.5,
  "curve": [
    { "years": 0.5, "yield_pct": 3.6 },
    { "years": 1, "yield_pct": 3.8 },
    { "years": 2, "yield_pct": 4 },
    { "years": 3, "yield_pct": 4.2 },
    { "years": 5, "yield_pct": 4.5 },
    { "years": 10, "yield_pct": 5 }
  ]
}
```

## What the template gives

On these numbers, the ladder is worth 4983423.61 with a DV01 of 1909.3. Over 0.5 years its hypothetical return is 2.42% with roll-down and no shift, and a parallel rise of 72.04 bps at the horizon takes that to zero.

- Dirty value: 4,983,424
- Modified duration: 3.8313 years
- DV01: 1,909.3
- Convexity: 23.97
- Coupon carry over the horizon: 2.107% of dirty value
- Roll-down over the horizon: 0.2404% of dirty value
- Hypothetical horizon return: 2.4215%
- Shift that takes it to zero: 72.04 bps

Where it flips, with the other inputs held:

- `parallel_shift_bps` at 72.0433: A parallel yield move of this size at the horizon takes the ladder's 0.5-year return to zero, found by repricing every flow. Past it, toward higher yields, the return is negative.

## Formula

Flows at t_k = maturity - k / f. Dirty = sum CF_k / (1 + y / f)^(f t_k). Accrued = coupon per period * (1 - f * first t). Macaulay = sum t PV / dirty. Modified = Macaulay / (1 + y / f). Convexity = sum t (t + 1 / f) PV / dirty / (1 + y / f)^2. DV01 = modified * dirty * 0.0001. Estimate = dirty (-modified s + 0.5 convexity s^2). Horizon value = sum over flows after H of CF / (1 + y / f)^(f (t - H)).

## Assumptions

- Time is in plain years, and flows are counted back from maturity. A fraction of a period at the front is a stub.
- The stub is discounted with a fractional exponent, and accrued interest is straight-line across the coupon period.
- DV01, duration and convexity are on the dirty price. Convexity includes the 1 / f term that goes with periodic compounding.
- Each bond's yield shifts by the same amount in a shock. Book totals add the bonds, so book duration and convexity are value-weighted.
- Coupon carry is the coupon earned over the horizon on an accrual basis: coupons paid plus the change in accrued.
- Pull to par is the rest of the horizon return at unchanged yield.
- Roll-down moves each bond's yield by the curve's change from maturity to shortened maturity, by straight-line interpolation, flat past the ends. The bond's own gap to the curve stays the same.
- Cash paid inside the horizon sits idle, with no reinvestment. Horizon returns aren't annualized.
- The shift that flips the horizon return is applied at the horizon date, on top of roll-down, and found by repricing every flow.

## Not modeled

- Reinvestment of coupons and of any principal paid back inside the horizon.
- Moves that aren't parallel, other than the one roll-down path.
- Default, calls, amortization, inflation links and floating coupons.
- Day counts, settlement lags and holiday calendars.
- Repo or other financing of the ladder, bid-ask, fees and 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/rates_ladder \
  -H 'content-type: application/json' \
  -d '{"bonds":[{"name":"Bond 1.25y","face":1000000,"coupon_pct":3.5,"maturity_years":1.25,"yield_pct":3.9,"freq":2},{"name":"Bond 2.5y","face":1000000,"coupon_pct":4,"maturity_years":2.5,"yield_pct":4.1,"freq":2},{"name":"Bond 3.75y","face":1000000,"coupon_pct":4,"maturity_years":3.75,"yield_pct":4.3,"freq":4},{"name":"Bond 5y","face":1000000,"coupon_pct":4.5,"maturity_years":5,"yield_pct":4.5,"freq":1},{"name":"Bond 10y","face":1000000,"coupon_pct":5,"maturity_years":10,"yield_pct":5.1,"freq":2}],"shocks_bps":[-100,-50,50,100,200],"horizon_years":0.5,"curve":[{"years":0.5,"yield_pct":3.6},{"years":1,"yield_pct":3.8},{"years":2,"yield_pct":4},{"years":3,"yield_pct":4.2},{"years":5,"yield_pct":4.5},{"years":10,"yield_pct":5}]}' \
  | jq '{summary, flags, flips, not_modeled, inputs_hash}'
```

### MCP

The tool is `rates_ladder` 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":"rates_ladder","arguments":{"bonds":[{"name":"Bond 1.25y","face":1000000,"coupon_pct":3.5,"maturity_years":1.25,"yield_pct":3.9,"freq":2},{"name":"Bond 2.5y","face":1000000,"coupon_pct":4,"maturity_years":2.5,"yield_pct":4.1,"freq":2},{"name":"Bond 3.75y","face":1000000,"coupon_pct":4,"maturity_years":3.75,"yield_pct":4.3,"freq":4},{"name":"Bond 5y","face":1000000,"coupon_pct":4.5,"maturity_years":5,"yield_pct":4.5,"freq":1},{"name":"Bond 10y","face":1000000,"coupon_pct":5,"maturity_years":10,"yield_pct":5.1,"freq":2}],"shocks_bps":[-100,-50,50,100,200],"horizon_years":0.5,"curve":[{"years":0.5,"yield_pct":3.6},{"years":1,"yield_pct":3.8},{"years":2,"yield_pct":4},{"years":3,"yield_pct":4.2},{"years":5,"yield_pct":4.5},{"years":10,"yield_pct":5}]}}}' \
  | 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 rates_ladder > mine.json           # replace every number in it
node hedgefund.mjs run rates_ladder mine.json | jq '{summary, flags, flips, not_modeled, inputs_hash}'
```

In a script:

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

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