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/__init__.py +243 -0
- capstack/balance_sheet.py +295 -0
- capstack/cli.py +1995 -0
- capstack/covenants.py +594 -0
- capstack/daycount.py +121 -0
- capstack/debt.py +2003 -0
- capstack/drivers.py +167 -0
- capstack/events.py +914 -0
- capstack/fees.py +484 -0
- capstack/incentive.py +517 -0
- capstack/money.py +204 -0
- capstack/operating.py +677 -0
- capstack/outcome.py +958 -0
- capstack/periods.py +431 -0
- capstack/py.typed +0 -0
- capstack/report.py +1188 -0
- capstack/returns.py +426 -0
- capstack/sensitivity.py +940 -0
- capstack/spec.py +1686 -0
- capstack/tax.py +834 -0
- capstack/transaction.py +430 -0
- capstack_lbo-0.1.0.dist-info/METADATA +1260 -0
- capstack_lbo-0.1.0.dist-info/RECORD +27 -0
- capstack_lbo-0.1.0.dist-info/WHEEL +5 -0
- capstack_lbo-0.1.0.dist-info/entry_points.txt +3 -0
- capstack_lbo-0.1.0.dist-info/licenses/LICENSE +21 -0
- capstack_lbo-0.1.0.dist-info/top_level.txt +1 -0
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
|
+
)
|