capstack-lbo 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
capstack/incentive.py ADDED
@@ -0,0 +1,517 @@
1
+ """The management incentive plan: what the people running the business are paid.
2
+
3
+ Every layer below this one describes a deal in which management is paid nothing
4
+ for five years of work, which is not a structure anybody signs. The plan sits
5
+ between the preferred claims and the residual split, and it takes its share out
6
+ of the common — the sponsor's own return included.
7
+
8
+ Three things have to be right for the number to mean anything.
9
+
10
+ *It is an option, not a share.* A pool struck at the equity value at close earns
11
+ on value created after close and nothing else. The arithmetic is the treasury
12
+ method: the exercise proceeds join the pot before the pot is divided, so a pool
13
+ holding a tenth of the fully diluted equity is not holding a tenth of the
14
+ residual — it is holding a tenth of the residual plus its own strike, less the
15
+ strike. Below the point where those two are equal the pool is out of the money,
16
+ takes nothing, and dilutes nobody. That is a real discontinuity in the payoff and
17
+ it is the reason a plan can look expensive in the base case and cost nothing in
18
+ the downside.
19
+
20
+ *It vests.* An exit three years into a five-year vest leaves two years unvested.
21
+ The unvested part is forfeited here rather than accelerated, because acceleration
22
+ on a change of control is a term that gets negotiated rather than a fact about
23
+ how options work. Where it has been negotiated, it is a flag.
24
+
25
+ *It ratchets.* A flat share is the simple case and the less common one. Most
26
+ plans step the pool up as the sponsor clears hurdles, and writing that down
27
+ naively produces a circle: the pool's share depends on the sponsor's return,
28
+ which depends on the pool's share. Models that implement the circle either
29
+ iterate to whatever they land on, or test the hurdle on a pre-dilution figure and
30
+ leave the reader to discover that 2.0x meant 2.1x before management were paid.
31
+
32
+ Neither is necessary. A ratchet written the way a well-drafted one reads — a
33
+ *marginal* share of the proceeds in each band above a hurdle, rather than a
34
+ retroactive share of everything — makes the sponsor's post-ratchet proceeds a
35
+ continuous, strictly increasing function of the pot. Each band's upper boundary
36
+ can then be solved in closed form from the boundary below it, in order, from the
37
+ bottom up. No iteration, no ambiguity, and the hurdles mean what they say: at the
38
+ pot where the second band opens, the sponsor has received exactly 2.0x on what it
39
+ actually takes home.
40
+
41
+ The retroactive alternative — the pool's higher share applying to the whole pot
42
+ once a hurdle is cleared — is a genuine structure and it is deliberately not
43
+ modelled, because it is discontinuous. A penny more of enterprise value can leave
44
+ the sponsor with less money than it had a penny earlier, and any solver asked
45
+ where the hurdle binds has to pick between two answers on either side of a cliff.
46
+ """
47
+
48
+ from __future__ import annotations
49
+
50
+ import itertools
51
+ from collections.abc import Iterator, Sequence
52
+ from dataclasses import dataclass
53
+
54
+ from .money import ONE, ZERO, Money, Numeric, is_close, money, safe_div
55
+
56
+ __all__ = [
57
+ "IncentiveError",
58
+ "OptionPool",
59
+ "PoolOutcome",
60
+ "Ratchet",
61
+ "RatchetBand",
62
+ "Vesting",
63
+ "settle_pool",
64
+ ]
65
+
66
+
67
+ class IncentiveError(ValueError):
68
+ """The plan as described cannot be settled."""
69
+
70
+
71
+ # --------------------------------------------------------------------------
72
+ # Vesting
73
+ # --------------------------------------------------------------------------
74
+
75
+ @dataclass(frozen=True, slots=True)
76
+ class Vesting:
77
+ """How much of the pool has been earned by the time the business is sold.
78
+
79
+ Straight-line from close over ``years``, with nothing earned before the
80
+ cliff and everything earned once the schedule has run. The cliff does not
81
+ slow the accrual — it withholds it: a one-year cliff on a four-year vest
82
+ pays a quarter of the pool on the day it lapses, not a quarter of a year's
83
+ worth.
84
+ """
85
+
86
+ years: Money
87
+ cliff_years: Money = ZERO
88
+ accelerates: bool = False
89
+
90
+ @classmethod
91
+ def of(
92
+ cls,
93
+ years: Numeric,
94
+ *,
95
+ cliff_years: Numeric = 0,
96
+ accelerates: bool = False,
97
+ ) -> Vesting:
98
+ return cls(
99
+ years=money(years),
100
+ cliff_years=money(cliff_years),
101
+ accelerates=accelerates,
102
+ )
103
+
104
+ def __post_init__(self) -> None:
105
+ if self.years <= 0:
106
+ raise IncentiveError("the vesting period must be positive")
107
+ if self.cliff_years < 0:
108
+ raise IncentiveError("the cliff must not be negative")
109
+ if self.cliff_years > self.years:
110
+ raise IncentiveError(
111
+ f"a cliff at {self.cliff_years} years on a {self.years}-year vest "
112
+ f"never lapses before the schedule finishes"
113
+ )
114
+
115
+ def vested_at(self, years: Money) -> Money:
116
+ """The share of the pool earned after ``years`` from close.
117
+
118
+ Acceleration is checked first and answers the question outright: a plan
119
+ that accelerates on a change of control vests in full at exit whatever
120
+ the schedule says, including before the cliff.
121
+ """
122
+ if self.accelerates:
123
+ return ONE
124
+ if years <= 0 or years < self.cliff_years:
125
+ return ZERO
126
+ if years >= self.years:
127
+ return ONE
128
+ return years / self.years
129
+
130
+
131
+ #: What a plan with no vesting schedule is worth at exit: all of it.
132
+ FULLY_VESTED: Money = ONE
133
+
134
+
135
+ # --------------------------------------------------------------------------
136
+ # The ratchet
137
+ # --------------------------------------------------------------------------
138
+
139
+ @dataclass(frozen=True, slots=True)
140
+ class RatchetBand:
141
+ """A marginal share of the pot, earned above a hurdle.
142
+
143
+ ``hurdle`` is a money multiple on the capital the ratchet is measured
144
+ against, and ``share`` is the fraction of *incremental* proceeds the pool
145
+ takes once that multiple has been reached. The first band's hurdle is zero:
146
+ it is the share the pool holds before any hurdle binds, which is commonly
147
+ but not always nothing.
148
+ """
149
+
150
+ hurdle: Money
151
+ share: Money
152
+
153
+ @classmethod
154
+ def of(cls, hurdle: Numeric, share: Numeric) -> RatchetBand:
155
+ return cls(hurdle=money(hurdle), share=money(share))
156
+
157
+ def __post_init__(self) -> None:
158
+ if self.hurdle < 0:
159
+ raise IncentiveError("a hurdle must not be negative")
160
+ if not (0 <= self.share < 1):
161
+ raise IncentiveError(
162
+ f"a marginal share of {self.share} leaves the holders it dilutes "
163
+ f"nothing at the margin; it must be at least 0 and below 1"
164
+ )
165
+
166
+
167
+ @dataclass(frozen=True, slots=True)
168
+ class Ratchet:
169
+ """Bands of marginal entitlement, opening as the measured capital clears hurdles.
170
+
171
+ ``measured_on`` names the instruments whose combined proceeds and capital
172
+ define the multiple the hurdles are read against. Left empty it means the
173
+ equity as a whole, which is the reading to use when the sponsor is the only
174
+ institutional holder. Naming a subset is how a plan that watches the
175
+ sponsor's own paper — and not the rollover sitting beside it — gets written
176
+ down.
177
+ """
178
+
179
+ bands: tuple[RatchetBand, ...]
180
+ measured_on: tuple[str, ...] = ()
181
+
182
+ @classmethod
183
+ def of(
184
+ cls,
185
+ bands: Sequence[tuple[Numeric, Numeric]] | Sequence[RatchetBand],
186
+ *,
187
+ measured_on: Sequence[str] = (),
188
+ ) -> Ratchet:
189
+ parsed: list[RatchetBand] = []
190
+ for band in bands:
191
+ if isinstance(band, RatchetBand):
192
+ parsed.append(band)
193
+ else:
194
+ hurdle, share = band
195
+ parsed.append(RatchetBand.of(hurdle, share))
196
+ return cls(bands=tuple(parsed), measured_on=tuple(measured_on))
197
+
198
+ def __post_init__(self) -> None:
199
+ if not self.bands:
200
+ raise IncentiveError("a ratchet needs at least one band")
201
+ if self.bands[0].hurdle != 0:
202
+ raise IncentiveError(
203
+ f"the first band opens at a multiple of {self.bands[0].hurdle}, so "
204
+ f"the pool's entitlement below that is undescribed; start the "
205
+ f"bands at 0"
206
+ )
207
+ for lower, upper in itertools.pairwise(self.bands):
208
+ if upper.hurdle <= lower.hurdle:
209
+ raise IncentiveError(
210
+ f"hurdles must step up: {upper.hurdle} does not come after "
211
+ f"{lower.hurdle}"
212
+ )
213
+ if upper.share < lower.share:
214
+ raise IncentiveError(
215
+ f"the band above {upper.hurdle}x pays {upper.share} where the "
216
+ f"one below pays {lower.share}; a plan that pays less for a "
217
+ f"better outcome is not a ratchet"
218
+ )
219
+ if len({name.strip() for name in self.measured_on}) != len(self.measured_on):
220
+ raise IncentiveError("the instruments a ratchet is measured on must be distinct")
221
+
222
+ def __len__(self) -> int:
223
+ return len(self.bands)
224
+
225
+ def __iter__(self) -> Iterator[RatchetBand]:
226
+ return iter(self.bands)
227
+
228
+ @property
229
+ def top_share(self) -> Money:
230
+ return self.bands[-1].share
231
+
232
+ def boundaries(
233
+ self,
234
+ *,
235
+ measured_capital: Money,
236
+ measured_prior: Money,
237
+ measured_ownership: Money,
238
+ vested: Money = FULLY_VESTED,
239
+ ) -> tuple[Money, ...]:
240
+ """The pot at which each band opens, from the bottom up.
241
+
242
+ ``measured_prior`` is what the measured instruments have already
243
+ received before the residual is touched — their preferred claims. It
244
+ counts towards the hurdle, because a money multiple is measured on
245
+ everything a holder receives and not only on the part that arrives last.
246
+
247
+ ``measured_ownership`` is their combined share of what is left after the
248
+ pool has taken its cut, which is the slope of their proceeds against the
249
+ pot inside a band.
250
+
251
+ The returned tuple has one entry per band. The first is always zero. A
252
+ band whose hurdle was already cleared by the bands below it opens at the
253
+ same pot as its predecessor and is therefore empty, which is the honest
254
+ answer rather than an error: a plan can be written with a hurdle that
255
+ cannot bite.
256
+ """
257
+ if measured_capital <= 0:
258
+ raise IncentiveError(
259
+ "the instruments a ratchet is measured on put in no capital, so "
260
+ "there is no multiple to test the hurdles against"
261
+ )
262
+
263
+ opens: list[Money] = [ZERO]
264
+ pot = ZERO
265
+ proceeds = measured_prior
266
+ for lower, upper in itertools.pairwise(self.bands):
267
+ target = upper.hurdle * measured_capital
268
+ slope = measured_ownership * (ONE - vested * lower.share)
269
+ if slope <= 0:
270
+ # The measured holders take nothing at the margin here, so no
271
+ # amount of further proceeds reaches the next hurdle. Every
272
+ # remaining band opens beyond any reachable pot.
273
+ opens.extend(_unreachable(len(self.bands) - len(opens)))
274
+ return tuple(opens)
275
+ width = safe_div(target - proceeds, slope, default=ZERO)
276
+ if width <= 0:
277
+ # Already cleared. The band opens where the one below it did,
278
+ # leaving it empty rather than pretending it starts earlier.
279
+ opens.append(pot)
280
+ continue
281
+ pot += width
282
+ proceeds = target
283
+ opens.append(pot)
284
+ return tuple(opens)
285
+
286
+ def entitlement(
287
+ self,
288
+ pot: Money,
289
+ *,
290
+ measured_capital: Money,
291
+ measured_prior: Money,
292
+ measured_ownership: Money,
293
+ vested: Money = FULLY_VESTED,
294
+ ) -> Money:
295
+ """What the pool is entitled to out of ``pot``, band by band."""
296
+ if pot <= 0:
297
+ return ZERO
298
+ opens = self.boundaries(
299
+ measured_capital=measured_capital,
300
+ measured_prior=measured_prior,
301
+ measured_ownership=measured_ownership,
302
+ vested=vested,
303
+ )
304
+ total = ZERO
305
+ for i, band in enumerate(self.bands):
306
+ start = opens[i]
307
+ if start >= pot:
308
+ break
309
+ end = opens[i + 1] if i + 1 < len(opens) else pot
310
+ width = min(end, pot) - start
311
+ if width > 0:
312
+ total += vested * band.share * width
313
+ return total
314
+
315
+
316
+ #: A pot no deal reaches, used to mark bands that can never open because the
317
+ #: holders the ratchet watches take nothing at the margin below them.
318
+ _UNREACHABLE: Money = money("1E30")
319
+
320
+
321
+ def _unreachable(count: int) -> list[Money]:
322
+ return [_UNREACHABLE for _ in range(max(count, 0))]
323
+
324
+
325
+ # --------------------------------------------------------------------------
326
+ # The pool
327
+ # --------------------------------------------------------------------------
328
+
329
+ @dataclass(frozen=True, slots=True)
330
+ class OptionPool:
331
+ """Options over a share of the fully diluted common, held by management.
332
+
333
+ ``share`` is that fraction, and it is what applies when there is no ratchet.
334
+ With a ratchet the bands describe the entitlement instead and ``share``
335
+ becomes the figure the plan is quoted at rather than the one it settles on;
336
+ it is still validated, because a file that describes both should not have
337
+ them disagree about what is possible.
338
+
339
+ ``strike`` is the aggregate cost of exercising the whole pool. Zero makes
340
+ the plan a straight sweet-equity stake in the residual, which is a real
341
+ structure and the one to model when management have bought their shares
342
+ rather than been granted options over them.
343
+ """
344
+
345
+ name: str
346
+ share: Money
347
+ strike: Money = ZERO
348
+ vesting: Vesting | None = None
349
+ ratchet: Ratchet | None = None
350
+
351
+ @classmethod
352
+ def of(
353
+ cls,
354
+ name: str,
355
+ share: Numeric,
356
+ *,
357
+ strike: Numeric = 0,
358
+ vesting: Vesting | None = None,
359
+ ratchet: Ratchet | None = None,
360
+ ) -> OptionPool:
361
+ return cls(
362
+ name=name,
363
+ share=money(share),
364
+ strike=money(strike),
365
+ vesting=vesting,
366
+ ratchet=ratchet,
367
+ )
368
+
369
+ def __post_init__(self) -> None:
370
+ if not self.name.strip():
371
+ raise IncentiveError("an incentive plan needs a name")
372
+ if not (0 <= self.share < 1):
373
+ raise IncentiveError(
374
+ f"{self.name}: the pool holds a share of the equity, so at least 0 "
375
+ f"and below 1; got {self.share}"
376
+ )
377
+ if self.strike < 0:
378
+ raise IncentiveError(f"{self.name}: the strike must not be negative")
379
+
380
+ def vested_at(self, years: Money) -> Money:
381
+ return FULLY_VESTED if self.vesting is None else self.vesting.vested_at(years)
382
+
383
+
384
+ @dataclass(frozen=True, slots=True)
385
+ class PoolOutcome:
386
+ """What the plan was worth, and what it cost the holders it diluted.
387
+
388
+ ``residual`` is what reached the common before the pool touched it.
389
+ ``pot`` is that plus the exercise proceeds where the options were exercised,
390
+ which is the amount the entitlement is measured against. ``paid`` is what
391
+ management take away after paying the strike, and ``dilution`` is the same
392
+ number seen from the other side: exactly what the common gave up.
393
+ """
394
+
395
+ pool: OptionPool
396
+ residual: Money
397
+ vested: Money
398
+ entitlement: Money
399
+ strike_paid: Money
400
+ exercised: bool
401
+
402
+ @property
403
+ def name(self) -> str:
404
+ return self.pool.name
405
+
406
+ @property
407
+ def pot(self) -> Money:
408
+ """The amount divided: the residual, enlarged by any strike paid in."""
409
+ return self.residual + self.strike_paid
410
+
411
+ @property
412
+ def paid(self) -> Money:
413
+ """What management receive, net of exercising."""
414
+ return self.entitlement - self.strike_paid
415
+
416
+ @property
417
+ def to_common(self) -> Money:
418
+ """What is left for the common holders after the plan."""
419
+ return self.pot - self.entitlement
420
+
421
+ @property
422
+ def dilution(self) -> Money:
423
+ """What the common gave up: the residual they would have had, less what they have."""
424
+ return self.residual - self.to_common
425
+
426
+ @property
427
+ def effective_share(self) -> Money:
428
+ """The pool's take as a share of the pot it was measured against.
429
+
430
+ With a flat plan this is the vested share. With a ratchet it is the
431
+ blended figure the bands produced, which is the number worth reporting
432
+ precisely because it is not one of the inputs.
433
+ """
434
+ return safe_div(self.entitlement, self.pot, default=ZERO)
435
+
436
+ @property
437
+ def is_in_the_money(self) -> bool:
438
+ return self.exercised
439
+
440
+ def reconciles(self, tolerance: Numeric = "1E-12") -> bool:
441
+ """Whether the two sides of the plan agree.
442
+
443
+ What management take and what the common give up are the same quantity
444
+ reached from opposite ends — one is the entitlement net of the strike,
445
+ the other is the residual net of what the common were left. They are
446
+ algebraically identical, which is why this is worth checking: the two
447
+ expressions associate their arithmetic differently, so the check is a
448
+ real one against the working precision rather than a tautology.
449
+ """
450
+ return is_close(self.paid, self.dilution, tolerance=tolerance)
451
+
452
+ @property
453
+ def forfeited_share(self) -> Money:
454
+ """The part of the pool that had not vested by the exit."""
455
+ return ONE - self.vested
456
+
457
+
458
+ def settle_pool(
459
+ pool: OptionPool,
460
+ residual: Money,
461
+ *,
462
+ years: Money,
463
+ measured_capital: Money = ZERO,
464
+ measured_prior: Money = ZERO,
465
+ measured_ownership: Money = ONE,
466
+ ) -> PoolOutcome:
467
+ """Settle the plan against the residual reaching the common.
468
+
469
+ The exercise decision is management's and it is taken on the arithmetic:
470
+ exercise where the entitlement exceeds the cost of exercising, and let the
471
+ options lapse where it does not. Both branches are computed rather than
472
+ assumed, and they agree at the boundary — an entitlement exactly equal to
473
+ the strike is worth nothing either way — so the payoff is continuous in the
474
+ residual even though the decision is not.
475
+
476
+ Vesting scales the pool and its strike together. Management pay for the
477
+ options they exercise and not for the ones they forfeited, so a half-vested
478
+ pool exercises half the strike.
479
+ """
480
+ if residual < 0:
481
+ raise IncentiveError("the residual reaching the common must not be negative")
482
+
483
+ vested = pool.vested_at(years)
484
+ strike = pool.strike * vested
485
+ pot = residual + strike
486
+
487
+ if pool.ratchet is not None:
488
+ entitlement = pool.ratchet.entitlement(
489
+ pot,
490
+ measured_capital=measured_capital,
491
+ measured_prior=measured_prior,
492
+ measured_ownership=measured_ownership,
493
+ vested=vested,
494
+ )
495
+ else:
496
+ entitlement = pot * pool.share * vested
497
+
498
+ if entitlement > strike:
499
+ return PoolOutcome(
500
+ pool=pool,
501
+ residual=residual,
502
+ vested=vested,
503
+ entitlement=entitlement,
504
+ strike_paid=strike,
505
+ exercised=True,
506
+ )
507
+
508
+ # Out of the money. The options lapse, nothing is paid in, nothing is taken
509
+ # out, and the common keep the residual whole.
510
+ return PoolOutcome(
511
+ pool=pool,
512
+ residual=residual,
513
+ vested=vested,
514
+ entitlement=ZERO,
515
+ strike_paid=ZERO,
516
+ exercised=False,
517
+ )