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/tax.py
ADDED
|
@@ -0,0 +1,834 @@
|
|
|
1
|
+
"""Tax on a business that is actually financed, rather than on one that is not.
|
|
2
|
+
|
|
3
|
+
The operating case computes tax on EBIT, which is the right thing for it to do:
|
|
4
|
+
it is an unlevered layer, and taxing it as though the structure did not exist
|
|
5
|
+
keeps it independent of the structure. But a leveraged company does not pay tax
|
|
6
|
+
on EBIT. It deducts its interest and pays tax on what is left, and for a deal
|
|
7
|
+
carrying five turns of debt that deduction is the second-largest cash line in
|
|
8
|
+
the model after EBITDA itself.
|
|
9
|
+
|
|
10
|
+
This module is the levered computation. It takes an interest series as its
|
|
11
|
+
input rather than a capital structure, which keeps it a pure sequential pass
|
|
12
|
+
over periods and makes it testable against interest written down by hand. The
|
|
13
|
+
circularity that follows — the deduction frees cash, the cash repays debt, the
|
|
14
|
+
smaller balance earns less interest, the smaller interest earns a smaller
|
|
15
|
+
deduction — is solved a layer up.
|
|
16
|
+
|
|
17
|
+
Three things make this domain logic rather than a subtraction.
|
|
18
|
+
|
|
19
|
+
*The deduction is capped, and a leveraged deal is where the cap binds.* A limit
|
|
20
|
+
of thirty per cent of earnings sounds generous until it is set against a
|
|
21
|
+
structure deliberately levered to the point where interest is a large fraction
|
|
22
|
+
of earnings, which is the entire design of the instrument. The cap is the
|
|
23
|
+
difference between a model that says leverage is free after tax and one that
|
|
24
|
+
says it stops being free somewhere, and where that somewhere is.
|
|
25
|
+
|
|
26
|
+
*The earnings the cap is struck against are a convention, not a fact.* The same
|
|
27
|
+
deal tested against thirty per cent of EBITDA and thirty per cent of EBIT gives
|
|
28
|
+
two different answers, because depreciation sits between them, and in a
|
|
29
|
+
capital-intensive business it sits between them heavily. Both bases are live
|
|
30
|
+
regimes. A model that hard-codes one of them cannot be asked the question.
|
|
31
|
+
|
|
32
|
+
*Disallowed interest is deferred, not lost, and it competes.* Interest the cap
|
|
33
|
+
refuses joins a pool, and the pool is tested again next period against next
|
|
34
|
+
period's cap, alongside next period's interest. So a deal can be inside the cap
|
|
35
|
+
on its own current interest and still have a disallowed balance, and a pool
|
|
36
|
+
accumulated in the early years can stay stuck for the whole hold. Treating the
|
|
37
|
+
disallowed amount as permanently lost overstates tax; treating it as freely
|
|
38
|
+
deductible later understates it.
|
|
39
|
+
|
|
40
|
+
*And the order against the loss pool matters.* The earnings base for the
|
|
41
|
+
interest cap is struck before any loss relief, so the cap is applied first, and
|
|
42
|
+
the carried-forward loss then shelters what the interest deduction has left
|
|
43
|
+
behind. Running the two the other way round gives a different cash tax in every
|
|
44
|
+
period where both bite, and the difference does not wash out over the hold
|
|
45
|
+
because the pools close at different balances.
|
|
46
|
+
|
|
47
|
+
Every figure here is an exact ``Decimal``. The pool movements are conservation
|
|
48
|
+
identities and are tested as equalities rather than tolerances: a unit of
|
|
49
|
+
interest is either deducted or in the disallowed pool, never approximately one
|
|
50
|
+
or the other.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
from __future__ import annotations
|
|
54
|
+
|
|
55
|
+
from collections.abc import Callable, Iterator, Sequence
|
|
56
|
+
from dataclasses import dataclass
|
|
57
|
+
from enum import Enum
|
|
58
|
+
|
|
59
|
+
from .debt import CapitalStructure, DebtSchedule
|
|
60
|
+
from .money import ZERO, Money, Numeric, money, safe_div
|
|
61
|
+
from .operating import DEFAULT_NOL_USAGE_LIMIT, OperatingAssumptions, OperatingModel
|
|
62
|
+
|
|
63
|
+
__all__ = [
|
|
64
|
+
"DEFAULT_INTEREST_LIMIT_RATE",
|
|
65
|
+
"DEFAULT_SOLVE_TOLERANCE",
|
|
66
|
+
"EarningsBase",
|
|
67
|
+
"InterestDeduction",
|
|
68
|
+
"InterestLimit",
|
|
69
|
+
"LeveredSolution",
|
|
70
|
+
"LeveredTaxPeriod",
|
|
71
|
+
"LeveredTaxSchedule",
|
|
72
|
+
"ScheduleRunner",
|
|
73
|
+
"TaxCircularityError",
|
|
74
|
+
"TaxPolicy",
|
|
75
|
+
"TaxPolicyError",
|
|
76
|
+
"levered_schedule",
|
|
77
|
+
"levered_tax",
|
|
78
|
+
"levered_tax_for_model",
|
|
79
|
+
"levered_tax_schedule",
|
|
80
|
+
"solve_levered",
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
#: How close two successive saving series must be for the solve to stop. The
|
|
84
|
+
#: quantity is money, so the default is a hundredth of a currency unit's
|
|
85
|
+
#: hundredth: far below anything a deal file expresses and far above the
|
|
86
|
+
#: precision the arithmetic carries.
|
|
87
|
+
DEFAULT_SOLVE_TOLERANCE = money("0.0001")
|
|
88
|
+
|
|
89
|
+
#: The maximum number of schedule runs a solve will make before giving up. The
|
|
90
|
+
#: measured worst case across the structures in the suite is well inside this;
|
|
91
|
+
#: see :func:`solve_levered`.
|
|
92
|
+
MAX_SOLVE_ITERATIONS = 100
|
|
93
|
+
|
|
94
|
+
#: The share of earnings a net interest deduction is commonly limited to. Thirty
|
|
95
|
+
#: per cent is the figure in the regime most leveraged deals are now struck
|
|
96
|
+
#: under; set the limit to ``None`` to model a regime with no cap at all.
|
|
97
|
+
DEFAULT_INTEREST_LIMIT_RATE = money("0.30")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class TaxPolicyError(ValueError):
|
|
101
|
+
"""A tax policy that cannot be applied as written."""
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class TaxCircularityError(RuntimeError):
|
|
105
|
+
"""The deduction and the debt it is charged on did not settle.
|
|
106
|
+
|
|
107
|
+
Distinct from :class:`~capstack.debt.CircularityError`, which is the inner
|
|
108
|
+
fixed point between a period's balance and the interest accrued on it. This
|
|
109
|
+
is the outer one, between the whole hold's tax and the whole hold's debt,
|
|
110
|
+
and a structure can solve the inner one in every period and still fail
|
|
111
|
+
here.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class EarningsBase(Enum):
|
|
116
|
+
"""Which earnings figure the interest cap is struck against.
|
|
117
|
+
|
|
118
|
+
The two differ by depreciation and amortisation, which is not a detail: in a
|
|
119
|
+
business running D&A at a tenth of revenue on a mid-single-digit margin, the
|
|
120
|
+
EBIT base is a small fraction of the EBITDA one and a structure comfortably
|
|
121
|
+
inside the cap on the first measure is nowhere near it on the second.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
#: Earnings before interest, tax, depreciation and amortisation.
|
|
125
|
+
EBITDA = "ebitda"
|
|
126
|
+
#: Earnings before interest and tax, so after depreciation and amortisation.
|
|
127
|
+
EBIT = "ebit"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass(frozen=True, slots=True)
|
|
131
|
+
class InterestLimit:
|
|
132
|
+
"""A cap on the interest deductible in a period.
|
|
133
|
+
|
|
134
|
+
``rate`` is applied to whichever earnings figure ``base`` names, and the
|
|
135
|
+
result is the most interest a period may deduct however much it incurred.
|
|
136
|
+
The earnings figure is taken before any loss relief, which is what fixes the
|
|
137
|
+
ordering against the loss pool.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
rate: Money
|
|
141
|
+
base: EarningsBase = EarningsBase.EBITDA
|
|
142
|
+
|
|
143
|
+
@classmethod
|
|
144
|
+
def of(
|
|
145
|
+
cls,
|
|
146
|
+
rate: Numeric = DEFAULT_INTEREST_LIMIT_RATE,
|
|
147
|
+
base: EarningsBase = EarningsBase.EBITDA,
|
|
148
|
+
) -> InterestLimit:
|
|
149
|
+
return cls(rate=money(rate), base=base)
|
|
150
|
+
|
|
151
|
+
def __post_init__(self) -> None:
|
|
152
|
+
if self.rate < 0:
|
|
153
|
+
raise TaxPolicyError("the interest limit rate must not be negative")
|
|
154
|
+
|
|
155
|
+
def allowance(self, *, ebitda: Money, ebit: Money) -> Money:
|
|
156
|
+
"""The most interest this period may deduct.
|
|
157
|
+
|
|
158
|
+
Negative earnings give an allowance of zero rather than a negative one:
|
|
159
|
+
a loss-making period does not owe the revenue authority a deduction, and
|
|
160
|
+
a negative allowance would turn the cap into a charge.
|
|
161
|
+
"""
|
|
162
|
+
earnings = ebitda if self.base is EarningsBase.EBITDA else ebit
|
|
163
|
+
return max(ZERO, earnings * self.rate)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@dataclass(frozen=True, slots=True)
|
|
167
|
+
class TaxPolicy:
|
|
168
|
+
"""The tax regime a case is run under.
|
|
169
|
+
|
|
170
|
+
``interest_limit`` of ``None`` means an uncapped deduction, which is both a
|
|
171
|
+
real historic regime and the useful comparison: running a case twice, once
|
|
172
|
+
capped and once not, prices the cap.
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
rate: Money
|
|
176
|
+
nol_usage_limit: Money = DEFAULT_NOL_USAGE_LIMIT
|
|
177
|
+
interest_limit: InterestLimit | None = None
|
|
178
|
+
#: Whether interest that accrues without being paid is deductible as it
|
|
179
|
+
#: accrues. A PIK margin rolled into the balance is an expense the period
|
|
180
|
+
#: incurred, and the common regimes deduct it on that basis; a regime that
|
|
181
|
+
#: waits for the cash sets this to ``False`` and defers it to the pool.
|
|
182
|
+
deduct_accrued_interest: bool = True
|
|
183
|
+
|
|
184
|
+
@classmethod
|
|
185
|
+
def from_assumptions(
|
|
186
|
+
cls,
|
|
187
|
+
assumptions: OperatingAssumptions,
|
|
188
|
+
*,
|
|
189
|
+
interest_limit: InterestLimit | None = None,
|
|
190
|
+
deduct_accrued_interest: bool = True,
|
|
191
|
+
) -> TaxPolicy:
|
|
192
|
+
"""Take the rate and the loss limit from the operating case itself.
|
|
193
|
+
|
|
194
|
+
The usual way to build a policy, and the safe one. The saving a solve
|
|
195
|
+
credits to the cash flow is measured against the operating case's own
|
|
196
|
+
tax line, so a policy whose rate disagrees with that case measures the
|
|
197
|
+
saving off the wrong base. Reading both off the assumptions makes the
|
|
198
|
+
disagreement impossible rather than merely detectable.
|
|
199
|
+
"""
|
|
200
|
+
return cls(
|
|
201
|
+
rate=assumptions.tax_rate,
|
|
202
|
+
nol_usage_limit=assumptions.nol_usage_limit,
|
|
203
|
+
interest_limit=interest_limit,
|
|
204
|
+
deduct_accrued_interest=deduct_accrued_interest,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
@classmethod
|
|
208
|
+
def of(
|
|
209
|
+
cls,
|
|
210
|
+
rate: Numeric,
|
|
211
|
+
*,
|
|
212
|
+
nol_usage_limit: Numeric = DEFAULT_NOL_USAGE_LIMIT,
|
|
213
|
+
interest_limit: InterestLimit | None = None,
|
|
214
|
+
deduct_accrued_interest: bool = True,
|
|
215
|
+
) -> TaxPolicy:
|
|
216
|
+
return cls(
|
|
217
|
+
rate=money(rate),
|
|
218
|
+
nol_usage_limit=money(nol_usage_limit),
|
|
219
|
+
interest_limit=interest_limit,
|
|
220
|
+
deduct_accrued_interest=deduct_accrued_interest,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
def __post_init__(self) -> None:
|
|
224
|
+
if self.rate < 0 or self.rate > 1:
|
|
225
|
+
raise TaxPolicyError("the tax rate must be between 0 and 1")
|
|
226
|
+
if self.nol_usage_limit < 0 or self.nol_usage_limit > 1:
|
|
227
|
+
raise TaxPolicyError("the loss usage limit must be between 0 and 1")
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@dataclass(frozen=True, slots=True)
|
|
231
|
+
class InterestDeduction:
|
|
232
|
+
"""What the cap did to one period's interest.
|
|
233
|
+
|
|
234
|
+
The whole movement is reported rather than only the deductible figure,
|
|
235
|
+
because the pool is the part a reader cannot reconstruct: a period deducting
|
|
236
|
+
its full current interest tells you nothing about whether a balance from
|
|
237
|
+
three years ago is still stuck behind the cap.
|
|
238
|
+
"""
|
|
239
|
+
|
|
240
|
+
#: Interest the period incurred and offered up for deduction.
|
|
241
|
+
arising: Money
|
|
242
|
+
#: Interest disallowed in earlier periods and tested again here.
|
|
243
|
+
carried_in: Money
|
|
244
|
+
#: The cap, or ``None`` where the regime has no cap.
|
|
245
|
+
allowance: Money | None
|
|
246
|
+
#: What was actually deducted against this period's income.
|
|
247
|
+
deductible: Money
|
|
248
|
+
#: What the cap refused, which joins the pool.
|
|
249
|
+
disallowed: Money
|
|
250
|
+
#: The pool after this period.
|
|
251
|
+
carried_out: Money
|
|
252
|
+
|
|
253
|
+
@property
|
|
254
|
+
def offered(self) -> Money:
|
|
255
|
+
"""Everything tested against the cap: this period's interest and the pool."""
|
|
256
|
+
return self.arising + self.carried_in
|
|
257
|
+
|
|
258
|
+
@property
|
|
259
|
+
def binding(self) -> bool:
|
|
260
|
+
"""Whether the cap refused anything that was offered to it."""
|
|
261
|
+
return self.disallowed > ZERO
|
|
262
|
+
|
|
263
|
+
@property
|
|
264
|
+
def headroom(self) -> Money:
|
|
265
|
+
"""Allowance left unused. Zero where the cap bound, or where there is none."""
|
|
266
|
+
if self.allowance is None:
|
|
267
|
+
return ZERO
|
|
268
|
+
return max(ZERO, self.allowance - self.offered)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@dataclass(frozen=True, slots=True)
|
|
272
|
+
class LeveredTaxPeriod:
|
|
273
|
+
"""One period of the levered tax computation, every line shown."""
|
|
274
|
+
|
|
275
|
+
index: int
|
|
276
|
+
ebitda: Money
|
|
277
|
+
ebit: Money
|
|
278
|
+
deduction: InterestDeduction
|
|
279
|
+
#: EBIT less the interest actually deducted, before any loss relief.
|
|
280
|
+
taxable_income: Money
|
|
281
|
+
loss_relief_used: Money
|
|
282
|
+
taxable_after_relief: Money
|
|
283
|
+
cash_tax: Money
|
|
284
|
+
closing_carryforward: Money
|
|
285
|
+
#: What this period would have paid with no interest deduction at all, from
|
|
286
|
+
#: the same opening loss pool. Carried rather than derived because it is the
|
|
287
|
+
#: only honest way to state the saving; see :attr:`shield`.
|
|
288
|
+
cash_tax_unlevered: Money
|
|
289
|
+
#: The rate the period was taxed at, kept so :attr:`naive_shield` can be
|
|
290
|
+
#: stated without dividing a tax by an income that may be zero.
|
|
291
|
+
rate: Money
|
|
292
|
+
|
|
293
|
+
@property
|
|
294
|
+
def interest(self) -> Money:
|
|
295
|
+
"""Interest the period incurred, whether or not it was deductible."""
|
|
296
|
+
return self.deduction.arising
|
|
297
|
+
|
|
298
|
+
@property
|
|
299
|
+
def shield(self) -> Money:
|
|
300
|
+
"""Cash tax the interest deduction actually saved this period.
|
|
301
|
+
|
|
302
|
+
Not the deduction multiplied by the rate. That figure is right only
|
|
303
|
+
where the period is profitable before and after the deduction and the
|
|
304
|
+
loss pool is not in play, and it overstates the saving everywhere else:
|
|
305
|
+
a deduction that takes a period from a small profit into a loss saves
|
|
306
|
+
tax on the profit and nothing on the rest of itself, and a deduction
|
|
307
|
+
taken in a period already sheltered by relief saves a fraction of the
|
|
308
|
+
rate. So the saving is the difference against the same period computed
|
|
309
|
+
with no deduction from the same opening pool, which is exact.
|
|
310
|
+
"""
|
|
311
|
+
return self.cash_tax_unlevered - self.cash_tax
|
|
312
|
+
|
|
313
|
+
@property
|
|
314
|
+
def naive_shield(self) -> Money:
|
|
315
|
+
"""Deduction times rate: what a model that stops at the interest line books.
|
|
316
|
+
|
|
317
|
+
Kept so the two can be compared rather than argued about. It is an upper
|
|
318
|
+
bound on :attr:`shield`, and the gap between them is the saving such a
|
|
319
|
+
model books and never receives.
|
|
320
|
+
"""
|
|
321
|
+
return self.deduction.deductible * self.rate
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
@dataclass(frozen=True, slots=True)
|
|
325
|
+
class LeveredTaxSchedule:
|
|
326
|
+
"""The levered tax computation across the hold, with the pools rolled."""
|
|
327
|
+
|
|
328
|
+
periods: tuple[LeveredTaxPeriod, ...]
|
|
329
|
+
opening_carryforward: Money
|
|
330
|
+
opening_disallowed_interest: Money
|
|
331
|
+
|
|
332
|
+
def __len__(self) -> int:
|
|
333
|
+
return len(self.periods)
|
|
334
|
+
|
|
335
|
+
def __iter__(self) -> Iterator[LeveredTaxPeriod]:
|
|
336
|
+
return iter(self.periods)
|
|
337
|
+
|
|
338
|
+
def __getitem__(self, index: int) -> LeveredTaxPeriod:
|
|
339
|
+
return self.periods[index]
|
|
340
|
+
|
|
341
|
+
@property
|
|
342
|
+
def cash_taxes(self) -> tuple[Money, ...]:
|
|
343
|
+
"""The cash tax series, which is what the cash flow needs from here."""
|
|
344
|
+
return tuple(p.cash_tax for p in self.periods)
|
|
345
|
+
|
|
346
|
+
@property
|
|
347
|
+
def total_cash_tax(self) -> Money:
|
|
348
|
+
return sum((p.cash_tax for p in self.periods), ZERO)
|
|
349
|
+
|
|
350
|
+
@property
|
|
351
|
+
def total_cash_tax_unlevered(self) -> Money:
|
|
352
|
+
"""What the hold would have paid with no interest deduction at all."""
|
|
353
|
+
return sum((p.cash_tax_unlevered for p in self.periods), ZERO)
|
|
354
|
+
|
|
355
|
+
@property
|
|
356
|
+
def total_shield(self) -> Money:
|
|
357
|
+
"""Cash tax the structure saved over the hold.
|
|
358
|
+
|
|
359
|
+
The difference of two totals, not the sum of the per-period
|
|
360
|
+
differences. The two are equal in exact arithmetic and are not quite
|
|
361
|
+
equal here: five roundings of a period difference against two roundings
|
|
362
|
+
of a total, in a context carrying 34 digits, leave the two orderings
|
|
363
|
+
disagreeing in the last places — measured at 7e-32 on the reference
|
|
364
|
+
case. Which is nothing, but it is not zero, and a reader reconciling
|
|
365
|
+
this against a cash flow should know which ordering produced it.
|
|
366
|
+
|
|
367
|
+
This ordering is the one chosen because it is the one that ties: both
|
|
368
|
+
totals are figures the model reports elsewhere, so the saving can be
|
|
369
|
+
checked against two numbers a reader already has.
|
|
370
|
+
"""
|
|
371
|
+
return self.total_cash_tax_unlevered - self.total_cash_tax
|
|
372
|
+
|
|
373
|
+
@property
|
|
374
|
+
def total_naive_shield(self) -> Money:
|
|
375
|
+
"""The saving a model that stops at the interest line would book."""
|
|
376
|
+
return sum((p.naive_shield for p in self.periods), ZERO)
|
|
377
|
+
|
|
378
|
+
@property
|
|
379
|
+
def total_interest_arising(self) -> Money:
|
|
380
|
+
return sum((p.deduction.arising for p in self.periods), ZERO)
|
|
381
|
+
|
|
382
|
+
@property
|
|
383
|
+
def total_interest_deducted(self) -> Money:
|
|
384
|
+
return sum((p.deduction.deductible for p in self.periods), ZERO)
|
|
385
|
+
|
|
386
|
+
@property
|
|
387
|
+
def closing_disallowed_interest(self) -> Money:
|
|
388
|
+
"""Interest still stuck behind the cap at the end of the hold."""
|
|
389
|
+
if not self.periods:
|
|
390
|
+
return self.opening_disallowed_interest
|
|
391
|
+
return self.periods[-1].deduction.carried_out
|
|
392
|
+
|
|
393
|
+
@property
|
|
394
|
+
def closing_carryforward(self) -> Money:
|
|
395
|
+
if not self.periods:
|
|
396
|
+
return self.opening_carryforward
|
|
397
|
+
return self.periods[-1].closing_carryforward
|
|
398
|
+
|
|
399
|
+
@property
|
|
400
|
+
def deducted_share(self) -> Money:
|
|
401
|
+
"""Share of the interest arising over the hold that was ever deducted.
|
|
402
|
+
|
|
403
|
+
The honest headline for a capped structure. A deal deducting four fifths
|
|
404
|
+
of its interest is paying tax on the other fifth of it, and that fifth
|
|
405
|
+
does not appear anywhere in a model that stops at the cash interest line.
|
|
406
|
+
"""
|
|
407
|
+
return safe_div(self.total_interest_deducted, self.total_interest_arising, default=ZERO)
|
|
408
|
+
|
|
409
|
+
@property
|
|
410
|
+
def binding_periods(self) -> tuple[int, ...]:
|
|
411
|
+
"""The indices of the periods where the cap refused something."""
|
|
412
|
+
return tuple(p.index for p in self.periods if p.deduction.binding)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _limit_interest(
|
|
416
|
+
*,
|
|
417
|
+
arising: Money,
|
|
418
|
+
carried_in: Money,
|
|
419
|
+
ebitda: Money,
|
|
420
|
+
ebit: Money,
|
|
421
|
+
limit: InterestLimit | None,
|
|
422
|
+
) -> InterestDeduction:
|
|
423
|
+
"""Test a period's interest, and the pool behind it, against the cap.
|
|
424
|
+
|
|
425
|
+
Current interest and the pool are tested together rather than in order,
|
|
426
|
+
because the pool carries forward indefinitely and is not tracked by vintage:
|
|
427
|
+
with no expiry to race, which of the two is deemed deducted first changes no
|
|
428
|
+
cash tax in any period and no closing balance.
|
|
429
|
+
"""
|
|
430
|
+
offered = arising + carried_in
|
|
431
|
+
if limit is None:
|
|
432
|
+
return InterestDeduction(
|
|
433
|
+
arising=arising,
|
|
434
|
+
carried_in=carried_in,
|
|
435
|
+
allowance=None,
|
|
436
|
+
deductible=offered,
|
|
437
|
+
disallowed=ZERO,
|
|
438
|
+
carried_out=ZERO,
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
allowance = limit.allowance(ebitda=ebitda, ebit=ebit)
|
|
442
|
+
deductible = min(offered, allowance)
|
|
443
|
+
return InterestDeduction(
|
|
444
|
+
arising=arising,
|
|
445
|
+
carried_in=carried_in,
|
|
446
|
+
allowance=allowance,
|
|
447
|
+
deductible=deductible,
|
|
448
|
+
disallowed=offered - deductible,
|
|
449
|
+
carried_out=offered - deductible,
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
@dataclass(frozen=True, slots=True)
|
|
454
|
+
class _Relief:
|
|
455
|
+
"""The loss pool's effect on one income figure."""
|
|
456
|
+
|
|
457
|
+
used: Money
|
|
458
|
+
after_relief: Money
|
|
459
|
+
cash_tax: Money
|
|
460
|
+
closing_carryforward: Money
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _apply_relief(taxable_income: Money, pool: Money, policy: TaxPolicy) -> _Relief:
|
|
464
|
+
"""Shelter an income figure with the loss pool, subject to the usage limit.
|
|
465
|
+
|
|
466
|
+
Shared by the real computation and the no-deduction counterfactual beside
|
|
467
|
+
it, so that the saving the deduction produces is a difference between two
|
|
468
|
+
runs of the same arithmetic rather than between two implementations of it.
|
|
469
|
+
"""
|
|
470
|
+
if taxable_income <= 0:
|
|
471
|
+
return _Relief(
|
|
472
|
+
used=ZERO,
|
|
473
|
+
after_relief=ZERO,
|
|
474
|
+
cash_tax=ZERO,
|
|
475
|
+
closing_carryforward=pool - taxable_income,
|
|
476
|
+
)
|
|
477
|
+
used = min(pool, taxable_income * policy.nol_usage_limit)
|
|
478
|
+
after_relief = taxable_income - used
|
|
479
|
+
return _Relief(
|
|
480
|
+
used=used,
|
|
481
|
+
after_relief=after_relief,
|
|
482
|
+
cash_tax=after_relief * policy.rate,
|
|
483
|
+
closing_carryforward=pool - used,
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def levered_tax(
|
|
488
|
+
*,
|
|
489
|
+
index: int,
|
|
490
|
+
ebitda: Money,
|
|
491
|
+
ebit: Money,
|
|
492
|
+
cash_interest: Money,
|
|
493
|
+
accrued_interest: Money = ZERO,
|
|
494
|
+
opening_carryforward: Money = ZERO,
|
|
495
|
+
opening_disallowed_interest: Money = ZERO,
|
|
496
|
+
policy: TaxPolicy,
|
|
497
|
+
) -> LeveredTaxPeriod:
|
|
498
|
+
"""Compute one period's cash tax on a levered basis.
|
|
499
|
+
|
|
500
|
+
The order is fixed and is the point of the function: the cap is struck
|
|
501
|
+
against earnings taken before relief, the interest deduction is taken
|
|
502
|
+
against EBIT, and the loss pool then shelters what remains. Each step reads
|
|
503
|
+
the output of the one before it, and swapping any two changes the answer.
|
|
504
|
+
"""
|
|
505
|
+
if cash_interest < 0 or accrued_interest < 0:
|
|
506
|
+
raise TaxPolicyError("interest must not be negative")
|
|
507
|
+
if opening_carryforward < 0:
|
|
508
|
+
raise TaxPolicyError("the carryforward pool must not be negative")
|
|
509
|
+
if opening_disallowed_interest < 0:
|
|
510
|
+
raise TaxPolicyError("the disallowed interest pool must not be negative")
|
|
511
|
+
|
|
512
|
+
# Interest that accrues without being paid is an expense the period incurred.
|
|
513
|
+
# Where the regime declines to recognise it until it is paid, it is not
|
|
514
|
+
# offered to the cap at all and waits in the pool instead, which is why it
|
|
515
|
+
# is added to the carried balance rather than dropped.
|
|
516
|
+
if policy.deduct_accrued_interest:
|
|
517
|
+
arising = cash_interest + accrued_interest
|
|
518
|
+
deferred = ZERO
|
|
519
|
+
else:
|
|
520
|
+
arising = cash_interest
|
|
521
|
+
deferred = accrued_interest
|
|
522
|
+
|
|
523
|
+
deduction = _limit_interest(
|
|
524
|
+
arising=arising,
|
|
525
|
+
carried_in=opening_disallowed_interest,
|
|
526
|
+
ebitda=ebitda,
|
|
527
|
+
ebit=ebit,
|
|
528
|
+
limit=policy.interest_limit,
|
|
529
|
+
)
|
|
530
|
+
if deferred:
|
|
531
|
+
deduction = InterestDeduction(
|
|
532
|
+
arising=deduction.arising,
|
|
533
|
+
carried_in=deduction.carried_in,
|
|
534
|
+
allowance=deduction.allowance,
|
|
535
|
+
deductible=deduction.deductible,
|
|
536
|
+
disallowed=deduction.disallowed,
|
|
537
|
+
carried_out=deduction.carried_out + deferred,
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
# EBIT less the interest the cap allowed. A loss here joins the pool as the
|
|
541
|
+
# levered loss: interest the cap allowed is what made it a loss, so it is
|
|
542
|
+
# what the pool inherits.
|
|
543
|
+
taxable_income = ebit - deduction.deductible
|
|
544
|
+
relief = _apply_relief(taxable_income, opening_carryforward, policy)
|
|
545
|
+
|
|
546
|
+
# The same period with nothing deducted, from the same opening pool. This is
|
|
547
|
+
# the counterfactual the saving is measured against, and it is the figure
|
|
548
|
+
# the operating layer already computes for itself.
|
|
549
|
+
unlevered = _apply_relief(ebit, opening_carryforward, policy)
|
|
550
|
+
|
|
551
|
+
return LeveredTaxPeriod(
|
|
552
|
+
index=index,
|
|
553
|
+
ebitda=ebitda,
|
|
554
|
+
ebit=ebit,
|
|
555
|
+
deduction=deduction,
|
|
556
|
+
taxable_income=taxable_income,
|
|
557
|
+
loss_relief_used=relief.used,
|
|
558
|
+
taxable_after_relief=relief.after_relief,
|
|
559
|
+
cash_tax=relief.cash_tax,
|
|
560
|
+
closing_carryforward=relief.closing_carryforward,
|
|
561
|
+
cash_tax_unlevered=unlevered.cash_tax,
|
|
562
|
+
rate=policy.rate,
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def levered_tax_schedule(
|
|
567
|
+
*,
|
|
568
|
+
ebitda: Sequence[Money],
|
|
569
|
+
ebit: Sequence[Money],
|
|
570
|
+
cash_interest: Sequence[Money],
|
|
571
|
+
accrued_interest: Sequence[Money] | None = None,
|
|
572
|
+
indices: Sequence[int] | None = None,
|
|
573
|
+
policy: TaxPolicy,
|
|
574
|
+
opening_carryforward: Numeric = 0,
|
|
575
|
+
opening_disallowed_interest: Numeric = 0,
|
|
576
|
+
) -> LeveredTaxSchedule:
|
|
577
|
+
"""Run the levered computation across periods, rolling both pools.
|
|
578
|
+
|
|
579
|
+
Takes bare series rather than an operating model so that the pass can be
|
|
580
|
+
driven by interest written down by hand, which is how the cap and the pool
|
|
581
|
+
are tested without a solver in the way. See
|
|
582
|
+
:func:`levered_tax_for_model` for the usual entry point.
|
|
583
|
+
"""
|
|
584
|
+
accrued = list(accrued_interest) if accrued_interest is not None else [ZERO] * len(ebit)
|
|
585
|
+
if not (len(ebitda) == len(ebit) == len(cash_interest) == len(accrued)):
|
|
586
|
+
raise TaxPolicyError("the earnings and interest series must be the same length")
|
|
587
|
+
keys = list(indices) if indices is not None else list(range(1, len(ebit) + 1))
|
|
588
|
+
if len(keys) != len(ebit):
|
|
589
|
+
raise TaxPolicyError("one index per period is required")
|
|
590
|
+
|
|
591
|
+
pool = money(opening_carryforward)
|
|
592
|
+
disallowed = money(opening_disallowed_interest)
|
|
593
|
+
rows: list[LeveredTaxPeriod] = []
|
|
594
|
+
for i, key in enumerate(keys):
|
|
595
|
+
row = levered_tax(
|
|
596
|
+
index=key,
|
|
597
|
+
ebitda=ebitda[i],
|
|
598
|
+
ebit=ebit[i],
|
|
599
|
+
cash_interest=cash_interest[i],
|
|
600
|
+
accrued_interest=accrued[i],
|
|
601
|
+
opening_carryforward=pool,
|
|
602
|
+
opening_disallowed_interest=disallowed,
|
|
603
|
+
policy=policy,
|
|
604
|
+
)
|
|
605
|
+
pool = row.closing_carryforward
|
|
606
|
+
disallowed = row.deduction.carried_out
|
|
607
|
+
rows.append(row)
|
|
608
|
+
|
|
609
|
+
return LeveredTaxSchedule(
|
|
610
|
+
periods=tuple(rows),
|
|
611
|
+
opening_carryforward=money(opening_carryforward),
|
|
612
|
+
opening_disallowed_interest=money(opening_disallowed_interest),
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def levered_tax_for_model(
|
|
617
|
+
model: OperatingModel,
|
|
618
|
+
*,
|
|
619
|
+
cash_interest: Sequence[Money],
|
|
620
|
+
accrued_interest: Sequence[Money] | None = None,
|
|
621
|
+
policy: TaxPolicy,
|
|
622
|
+
opening_carryforward: Numeric = 0,
|
|
623
|
+
opening_disallowed_interest: Numeric = 0,
|
|
624
|
+
) -> LeveredTaxSchedule:
|
|
625
|
+
"""Run the levered computation against an operating case's earnings.
|
|
626
|
+
|
|
627
|
+
The earnings come from the model and the interest from the caller, which is
|
|
628
|
+
the shape the circularity above this layer needs: the solver holds the
|
|
629
|
+
operating case still and varies the interest.
|
|
630
|
+
"""
|
|
631
|
+
return levered_tax_schedule(
|
|
632
|
+
ebitda=[p.ebitda for p in model],
|
|
633
|
+
ebit=[p.ebit for p in model],
|
|
634
|
+
cash_interest=cash_interest,
|
|
635
|
+
accrued_interest=accrued_interest,
|
|
636
|
+
indices=[p.index for p in model],
|
|
637
|
+
policy=policy,
|
|
638
|
+
opening_carryforward=opening_carryforward,
|
|
639
|
+
opening_disallowed_interest=opening_disallowed_interest,
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
# -- The circularity -----------------------------------------------------
|
|
644
|
+
|
|
645
|
+
#: A function that runs the capital structure against a cash-flow series.
|
|
646
|
+
#:
|
|
647
|
+
#: The solve varies the cash flow and reads back the interest, and has no other
|
|
648
|
+
#: business with the schedule. Taking a runner rather than a structure is what
|
|
649
|
+
#: keeps recapitalisations, add-ons, refinancings and fee write-offs out of this
|
|
650
|
+
#: layer: the caller configures them once, closes over them, and the fixed point
|
|
651
|
+
#: never learns they exist.
|
|
652
|
+
ScheduleRunner = Callable[[Sequence[Money]], DebtSchedule]
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
@dataclass(frozen=True, slots=True)
|
|
656
|
+
class LeveredSolution:
|
|
657
|
+
"""A capital structure and a tax computation that agree with each other."""
|
|
658
|
+
|
|
659
|
+
schedule: DebtSchedule
|
|
660
|
+
tax: LeveredTaxSchedule
|
|
661
|
+
#: The cash flow the schedule was finally run against: the operating case's
|
|
662
|
+
#: unlevered free cash flow plus the saving the deduction produced.
|
|
663
|
+
cash_flows: tuple[Money, ...]
|
|
664
|
+
#: How many times the schedule was run, the first pass included.
|
|
665
|
+
iterations: int
|
|
666
|
+
#: The largest period-on-period movement in the saving when the solve
|
|
667
|
+
#: stopped. Reported rather than discarded, because a caller comparing two
|
|
668
|
+
#: structures to a hundredth of a turn should be able to see it.
|
|
669
|
+
residual: Money
|
|
670
|
+
|
|
671
|
+
@property
|
|
672
|
+
def total_shield(self) -> Money:
|
|
673
|
+
"""Cash tax the structure saved, net of the debt the saving repaid."""
|
|
674
|
+
return self.tax.total_shield
|
|
675
|
+
|
|
676
|
+
@property
|
|
677
|
+
def shields(self) -> tuple[Money, ...]:
|
|
678
|
+
return tuple(p.shield for p in self.tax)
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def _shield_series(model: OperatingModel, tax: LeveredTaxSchedule) -> list[Money]:
|
|
682
|
+
"""The saving per period, measured against the operating case's own tax line.
|
|
683
|
+
|
|
684
|
+
Not against the tax layer's internal counterfactual. The two agree — that
|
|
685
|
+
is checked before the solve starts — but the unlevered free cash flow the
|
|
686
|
+
saving is added to was struck net of the *model's* figure, so the model's
|
|
687
|
+
figure is the one the adjustment has to be measured from or the cash flow
|
|
688
|
+
double-counts the difference between them.
|
|
689
|
+
"""
|
|
690
|
+
return [period.tax.cash_tax - row.cash_tax for period, row in zip(model, tax, strict=True)]
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def solve_levered(
|
|
694
|
+
*,
|
|
695
|
+
model: OperatingModel,
|
|
696
|
+
run: ScheduleRunner,
|
|
697
|
+
policy: TaxPolicy,
|
|
698
|
+
opening_carryforward: Numeric = 0,
|
|
699
|
+
opening_disallowed_interest: Numeric = 0,
|
|
700
|
+
tolerance: Numeric = DEFAULT_SOLVE_TOLERANCE,
|
|
701
|
+
max_iterations: int = MAX_SOLVE_ITERATIONS,
|
|
702
|
+
) -> LeveredSolution:
|
|
703
|
+
"""Settle the interest deduction against the debt it is charged on.
|
|
704
|
+
|
|
705
|
+
Two sequential passes, alternated. The tax pass has to see the whole hold at
|
|
706
|
+
once because both pools carry forward, which is why this cannot be folded
|
|
707
|
+
into the schedule's own per-period fixed point: it sits outside it, and a
|
|
708
|
+
structure can solve the inner one in every period and still fail here.
|
|
709
|
+
|
|
710
|
+
The map contracts hard. One turn of the loop moves the saving by the tax
|
|
711
|
+
rate times the interest on the cash the last turn freed, so the residual
|
|
712
|
+
falls by roughly ``rate x coupon x year_fraction`` each time — a few tenths
|
|
713
|
+
of a per cent at any realistic combination — and the feedback is negative:
|
|
714
|
+
a larger deduction frees cash, the cash repays debt, and the smaller balance
|
|
715
|
+
earns the smaller interest that shrinks the next deduction. Across the
|
|
716
|
+
structures in the suite no solve has taken more than four passes.
|
|
717
|
+
|
|
718
|
+
Before iterating, the solve checks its own counterfactual: run with no
|
|
719
|
+
interest at all, the tax pass must reproduce the operating case's tax line
|
|
720
|
+
period for period. A policy whose rate or loss limit disagrees with the case
|
|
721
|
+
it is handed fails that check and is refused, because the saving credited to
|
|
722
|
+
the cash flow is a difference against that line and measuring it off a
|
|
723
|
+
different base would quietly misstate every period.
|
|
724
|
+
"""
|
|
725
|
+
if max_iterations < 1:
|
|
726
|
+
raise TaxPolicyError("a solve needs at least one iteration")
|
|
727
|
+
limit = money(tolerance)
|
|
728
|
+
if limit < 0:
|
|
729
|
+
raise TaxPolicyError("the tolerance must not be negative")
|
|
730
|
+
|
|
731
|
+
unlevered = levered_tax_for_model(
|
|
732
|
+
model,
|
|
733
|
+
cash_interest=[ZERO] * len(model),
|
|
734
|
+
policy=policy,
|
|
735
|
+
opening_carryforward=opening_carryforward,
|
|
736
|
+
opening_disallowed_interest=opening_disallowed_interest,
|
|
737
|
+
)
|
|
738
|
+
for period, row in zip(model, unlevered, strict=True):
|
|
739
|
+
if row.cash_tax != period.tax.cash_tax:
|
|
740
|
+
raise TaxPolicyError(
|
|
741
|
+
"this tax policy disagrees with the operating case it is being run "
|
|
742
|
+
f"against: with no interest at all, period {period.index} pays "
|
|
743
|
+
f"{row.cash_tax} here and {period.tax.cash_tax} in the projection. "
|
|
744
|
+
"Build the policy with TaxPolicy.from_assumptions so the rate and "
|
|
745
|
+
"the loss limit come from the case itself."
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
base = [p.unlevered_free_cash_flow for p in model]
|
|
749
|
+
shields = [ZERO] * len(model)
|
|
750
|
+
# The series the schedule in hand was actually run against, so that what is
|
|
751
|
+
# returned is self-consistent: the schedule, the tax read off it, and the
|
|
752
|
+
# cash flow that produced it all describe the same pass.
|
|
753
|
+
applied = list(base)
|
|
754
|
+
schedule = run(applied)
|
|
755
|
+
tax = unlevered
|
|
756
|
+
residual = ZERO
|
|
757
|
+
iterations = 1
|
|
758
|
+
|
|
759
|
+
while True:
|
|
760
|
+
tax = levered_tax_for_model(
|
|
761
|
+
model,
|
|
762
|
+
cash_interest=[p.cash_interest for p in schedule],
|
|
763
|
+
accrued_interest=[p.pik_interest for p in schedule],
|
|
764
|
+
policy=policy,
|
|
765
|
+
opening_carryforward=opening_carryforward,
|
|
766
|
+
opening_disallowed_interest=opening_disallowed_interest,
|
|
767
|
+
)
|
|
768
|
+
fresh = _shield_series(model, tax)
|
|
769
|
+
residual = max((abs(a - b) for a, b in zip(fresh, shields, strict=True)), default=ZERO)
|
|
770
|
+
shields = fresh
|
|
771
|
+
if residual <= limit:
|
|
772
|
+
# The schedule in hand was run against the previous saving series,
|
|
773
|
+
# which this one is within tolerance of, so it is the answer. The
|
|
774
|
+
# flows returned are the ones that produced it, not the ones just
|
|
775
|
+
# computed, which no schedule has been run against.
|
|
776
|
+
return LeveredSolution(
|
|
777
|
+
schedule=schedule,
|
|
778
|
+
tax=tax,
|
|
779
|
+
cash_flows=tuple(applied),
|
|
780
|
+
iterations=iterations,
|
|
781
|
+
residual=residual,
|
|
782
|
+
)
|
|
783
|
+
flows = [cf + s for cf, s in zip(base, shields, strict=True)]
|
|
784
|
+
if iterations >= max_iterations:
|
|
785
|
+
raise TaxCircularityError(
|
|
786
|
+
f"the interest deduction and the debt it is charged on did not "
|
|
787
|
+
f"settle in {max_iterations} passes; the saving was still moving "
|
|
788
|
+
f"by {residual} when the solve stopped"
|
|
789
|
+
)
|
|
790
|
+
applied = flows
|
|
791
|
+
schedule = run(applied)
|
|
792
|
+
iterations += 1
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
def levered_schedule(
|
|
796
|
+
structure: CapitalStructure,
|
|
797
|
+
model: OperatingModel,
|
|
798
|
+
*,
|
|
799
|
+
policy: TaxPolicy,
|
|
800
|
+
opening_cash: Numeric = 0,
|
|
801
|
+
opening_ebitda: Numeric | None = None,
|
|
802
|
+
opening_carryforward: Numeric = 0,
|
|
803
|
+
opening_disallowed_interest: Numeric = 0,
|
|
804
|
+
tolerance: Numeric = DEFAULT_SOLVE_TOLERANCE,
|
|
805
|
+
max_iterations: int = MAX_SOLVE_ITERATIONS,
|
|
806
|
+
) -> LeveredSolution:
|
|
807
|
+
"""Run a structure against a case, with the deduction settled against it.
|
|
808
|
+
|
|
809
|
+
The plain entry point, for a structure with no events attached to it. A deal
|
|
810
|
+
with recapitalisations, add-ons or refinancings builds its own runner and
|
|
811
|
+
calls :func:`solve_levered` directly, which is what ``Deal.levered`` does.
|
|
812
|
+
"""
|
|
813
|
+
ebitda = [model.trailing_ebitda(i) for i in range(len(model))]
|
|
814
|
+
anchor = opening_ebitda if opening_ebitda is not None else model.entry_ebitda
|
|
815
|
+
|
|
816
|
+
def run(cash_flows: Sequence[Money]) -> DebtSchedule:
|
|
817
|
+
return DebtSchedule.run(
|
|
818
|
+
structure,
|
|
819
|
+
[p.period for p in model],
|
|
820
|
+
cash_flows,
|
|
821
|
+
opening_cash=opening_cash,
|
|
822
|
+
ebitda=ebitda,
|
|
823
|
+
opening_ebitda=anchor,
|
|
824
|
+
)
|
|
825
|
+
|
|
826
|
+
return solve_levered(
|
|
827
|
+
model=model,
|
|
828
|
+
run=run,
|
|
829
|
+
policy=policy,
|
|
830
|
+
opening_carryforward=opening_carryforward,
|
|
831
|
+
opening_disallowed_interest=opening_disallowed_interest,
|
|
832
|
+
tolerance=tolerance,
|
|
833
|
+
max_iterations=max_iterations,
|
|
834
|
+
)
|