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 ADDED
@@ -0,0 +1,243 @@
1
+ """capstack — a leveraged buyout engine."""
2
+
3
+ from .balance_sheet import (
4
+ BalanceSheetError,
5
+ OpeningBalanceSheet,
6
+ PurchaseAccounting,
7
+ TargetBookBalanceSheet,
8
+ UnbalancedBalanceSheetError,
9
+ )
10
+ from .covenants import (
11
+ Covenant,
12
+ CovenantObservation,
13
+ CovenantReport,
14
+ Direction,
15
+ Measure,
16
+ )
17
+ from .daycount import DayCount, year_fraction
18
+ from .debt import (
19
+ CapitalStructure,
20
+ CircularityError,
21
+ DebtPeriod,
22
+ DebtSchedule,
23
+ InterestBasis,
24
+ SweepGrid,
25
+ SweepStep,
26
+ Tranche,
27
+ TrancheKind,
28
+ TranchePeriod,
29
+ )
30
+ from .drivers import Driver
31
+ from .events import (
32
+ AddOn,
33
+ AddOnError,
34
+ AddOnOutcome,
35
+ BlendedEntry,
36
+ Draw,
37
+ Recapitalisation,
38
+ RecapitalisationError,
39
+ RecapitalisationOutcome,
40
+ Refinancing,
41
+ RefinancingError,
42
+ RefinancingOutcome,
43
+ )
44
+ from .incentive import (
45
+ IncentiveError,
46
+ OptionPool,
47
+ PoolOutcome,
48
+ Ratchet,
49
+ RatchetBand,
50
+ Vesting,
51
+ settle_pool,
52
+ )
53
+ from .money import Money, allocate_pro_rata, money, quantize, rate, rescale
54
+ from .operating import (
55
+ AcquiredStream,
56
+ OperatingAssumptions,
57
+ OperatingModel,
58
+ OperatingPeriod,
59
+ TaxResult,
60
+ apply_carryforward,
61
+ )
62
+ from .outcome import (
63
+ Attribution,
64
+ Distribution,
65
+ ExitValuation,
66
+ Outcome,
67
+ Security,
68
+ SecurityKind,
69
+ SecurityOutcome,
70
+ default_securities,
71
+ )
72
+ from .periods import Frequency, Period, PeriodGrid
73
+ from .report import Report, Section
74
+ from .returns import (
75
+ AmbiguousIRRError,
76
+ CashFlow,
77
+ CashFlowStream,
78
+ IRRError,
79
+ NoSignChangeError,
80
+ cagr,
81
+ irr_periodic,
82
+ moic,
83
+ npv_periodic,
84
+ )
85
+ from .sensitivity import (
86
+ Axis,
87
+ Breakeven,
88
+ Case,
89
+ Cell,
90
+ Dimension,
91
+ Grid,
92
+ Metric,
93
+ SensitivityError,
94
+ Unit,
95
+ solve,
96
+ )
97
+ from .spec import (
98
+ Deal,
99
+ DealSpecError,
100
+ EquityPlan,
101
+ Funding,
102
+ IncentivePlan,
103
+ SecurityPlan,
104
+ load_deal,
105
+ parse_deal,
106
+ )
107
+ from .tax import (
108
+ DEFAULT_INTEREST_LIMIT_RATE,
109
+ EarningsBase,
110
+ InterestDeduction,
111
+ InterestLimit,
112
+ LeveredTaxPeriod,
113
+ LeveredTaxSchedule,
114
+ TaxPolicy,
115
+ TaxPolicyError,
116
+ levered_tax,
117
+ levered_tax_for_model,
118
+ levered_tax_schedule,
119
+ )
120
+ from .transaction import (
121
+ DebtFunding,
122
+ EntryValuation,
123
+ LineItem,
124
+ SourcesAndUses,
125
+ Transaction,
126
+ UnbalancedTransactionError,
127
+ )
128
+
129
+ __version__ = "0.1.0"
130
+
131
+ __all__ = [
132
+ "DEFAULT_INTEREST_LIMIT_RATE",
133
+ "AcquiredStream",
134
+ "AddOn",
135
+ "AddOnError",
136
+ "AddOnOutcome",
137
+ "AmbiguousIRRError",
138
+ "Attribution",
139
+ "Axis",
140
+ "BalanceSheetError",
141
+ "BlendedEntry",
142
+ "Breakeven",
143
+ "CapitalStructure",
144
+ "Case",
145
+ "CashFlow",
146
+ "CashFlowStream",
147
+ "Cell",
148
+ "CircularityError",
149
+ "Covenant",
150
+ "CovenantObservation",
151
+ "CovenantReport",
152
+ "DayCount",
153
+ "Deal",
154
+ "DealSpecError",
155
+ "DebtFunding",
156
+ "DebtPeriod",
157
+ "DebtSchedule",
158
+ "Dimension",
159
+ "Direction",
160
+ "Distribution",
161
+ "Draw",
162
+ "Driver",
163
+ "EarningsBase",
164
+ "EntryValuation",
165
+ "EquityPlan",
166
+ "ExitValuation",
167
+ "Frequency",
168
+ "Funding",
169
+ "Grid",
170
+ "IRRError",
171
+ "IncentiveError",
172
+ "IncentivePlan",
173
+ "InterestBasis",
174
+ "InterestDeduction",
175
+ "InterestLimit",
176
+ "LeveredTaxPeriod",
177
+ "LeveredTaxSchedule",
178
+ "LineItem",
179
+ "Measure",
180
+ "Metric",
181
+ "Money",
182
+ "NoSignChangeError",
183
+ "OpeningBalanceSheet",
184
+ "OperatingAssumptions",
185
+ "OperatingModel",
186
+ "OperatingPeriod",
187
+ "OptionPool",
188
+ "Outcome",
189
+ "Period",
190
+ "PeriodGrid",
191
+ "PoolOutcome",
192
+ "PurchaseAccounting",
193
+ "Ratchet",
194
+ "RatchetBand",
195
+ "Recapitalisation",
196
+ "RecapitalisationError",
197
+ "RecapitalisationOutcome",
198
+ "Refinancing",
199
+ "RefinancingError",
200
+ "RefinancingOutcome",
201
+ "Report",
202
+ "Section",
203
+ "Security",
204
+ "SecurityKind",
205
+ "SecurityOutcome",
206
+ "SecurityPlan",
207
+ "SensitivityError",
208
+ "SourcesAndUses",
209
+ "SweepGrid",
210
+ "SweepStep",
211
+ "TargetBookBalanceSheet",
212
+ "TaxPolicy",
213
+ "TaxPolicyError",
214
+ "TaxResult",
215
+ "Tranche",
216
+ "TrancheKind",
217
+ "TranchePeriod",
218
+ "Transaction",
219
+ "UnbalancedBalanceSheetError",
220
+ "UnbalancedTransactionError",
221
+ "Unit",
222
+ "Vesting",
223
+ "__version__",
224
+ "allocate_pro_rata",
225
+ "apply_carryforward",
226
+ "cagr",
227
+ "default_securities",
228
+ "irr_periodic",
229
+ "levered_tax",
230
+ "levered_tax_for_model",
231
+ "levered_tax_schedule",
232
+ "load_deal",
233
+ "moic",
234
+ "money",
235
+ "npv_periodic",
236
+ "parse_deal",
237
+ "quantize",
238
+ "rate",
239
+ "rescale",
240
+ "settle_pool",
241
+ "solve",
242
+ "year_fraction",
243
+ ]
@@ -0,0 +1,295 @@
1
+ """The balance sheet the target carries out of close.
2
+
3
+ A buyout does not leave the target's balance sheet where it found it. The old
4
+ capital structure is repaid, a new one is put in its place, the assets are
5
+ re-measured at what was just paid for them, and the difference between the
6
+ price and the identifiable net assets acquired becomes goodwill. Everything
7
+ downstream — the debt schedule, the exit equity bridge, every covenant that
8
+ references a balance — starts from this statement rather than from the target's
9
+ last audited accounts.
10
+
11
+ Three points are where an opening balance sheet usually goes wrong.
12
+
13
+ *The target's own goodwill does not survive.* Goodwill is the residual of a
14
+ previous transaction. It is written off and a new residual is struck against
15
+ the price just paid; carrying both counts the same intangible twice.
16
+
17
+ *A step-up that is not deductible creates a deferred tax liability.* Writing an
18
+ asset up to fair value in a stock deal raises book depreciation without raising
19
+ tax depreciation, so the model owes tax on the difference for the rest of the
20
+ asset's life. Recognising the step-up without the liability overstates equity
21
+ by the tax on it and understates goodwill by the same amount.
22
+
23
+ *Debt is carried at face.* Issue discount and financing fees are shown against
24
+ it as unamortised balances rather than netted into it, because interest accrues
25
+ on what is owed rather than on what was received, and the amortisation of those
26
+ balances is a separate charge with its own life.
27
+
28
+ The identity is enforced, not checked. Assets equal liabilities plus equity to
29
+ the cent or the object does not exist, which is the same discipline the funding
30
+ table holds itself to and for the same reason: an opening balance sheet that is
31
+ out by a small amount is not nearly right, it has a line item missing.
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ from dataclasses import dataclass
37
+
38
+ from .money import ZERO, Money, Numeric, money
39
+ from .transaction import Transaction
40
+
41
+ __all__ = [
42
+ "BalanceSheetError",
43
+ "OpeningBalanceSheet",
44
+ "PurchaseAccounting",
45
+ "TargetBookBalanceSheet",
46
+ "UnbalancedBalanceSheetError",
47
+ ]
48
+
49
+
50
+ class BalanceSheetError(ValueError):
51
+ """The book position given cannot be reconciled with the transaction."""
52
+
53
+
54
+ class UnbalancedBalanceSheetError(BalanceSheetError):
55
+ """Assets do not equal liabilities plus equity."""
56
+
57
+
58
+ @dataclass(frozen=True, slots=True)
59
+ class TargetBookBalanceSheet:
60
+ """The target's own balance sheet, the day before close.
61
+
62
+ Totals rather than a full chart of accounts: purchase accounting only needs
63
+ to know what the book equity is, how much of the asset side is goodwill
64
+ that will be written off, and how the rest splits between cash and
65
+ everything else. The split matters because cash is the one asset that both
66
+ funds the deal and survives it unchanged.
67
+ """
68
+
69
+ total_assets: Money
70
+ total_liabilities: Money
71
+ goodwill: Money = ZERO
72
+
73
+ @classmethod
74
+ def of(
75
+ cls,
76
+ total_assets: Numeric,
77
+ total_liabilities: Numeric,
78
+ *,
79
+ goodwill: Numeric = 0,
80
+ ) -> TargetBookBalanceSheet:
81
+ return cls(
82
+ total_assets=money(total_assets),
83
+ total_liabilities=money(total_liabilities),
84
+ goodwill=money(goodwill),
85
+ )
86
+
87
+ def __post_init__(self) -> None:
88
+ if self.total_assets < 0:
89
+ raise BalanceSheetError("total assets must not be negative")
90
+ if self.total_liabilities < 0:
91
+ raise BalanceSheetError("total liabilities must not be negative")
92
+ if self.goodwill < 0:
93
+ raise BalanceSheetError("goodwill must not be negative")
94
+ if self.goodwill > self.total_assets:
95
+ raise BalanceSheetError(
96
+ f"goodwill ({self.goodwill}) exceeds total assets ({self.total_assets})"
97
+ )
98
+
99
+ @property
100
+ def book_equity(self) -> Money:
101
+ """What the accounts say the equity is worth.
102
+
103
+ Frequently negative for a business that has been through a previous
104
+ buyout, which is not an error and is exactly the case that produces a
105
+ large goodwill number.
106
+ """
107
+ return self.total_assets - self.total_liabilities
108
+
109
+
110
+ @dataclass(frozen=True, slots=True)
111
+ class PurchaseAccounting:
112
+ """How the price is allocated across what was bought.
113
+
114
+ ``step_up`` writes identifiable assets up to fair value. ``step_up_tax_rate``
115
+ is the rate at which the resulting book/tax difference will be taxed; set it
116
+ to zero for an asset deal, where the step-up is deductible and no deferred
117
+ liability arises.
118
+ """
119
+
120
+ step_up: Money = ZERO
121
+ step_up_tax_rate: Money = ZERO
122
+
123
+ @classmethod
124
+ def of(cls, *, step_up: Numeric = 0, step_up_tax_rate: Numeric = 0) -> PurchaseAccounting:
125
+ return cls(step_up=money(step_up), step_up_tax_rate=money(step_up_tax_rate))
126
+
127
+ def __post_init__(self) -> None:
128
+ if self.step_up < 0:
129
+ raise BalanceSheetError("the asset step-up must not be negative")
130
+ if not (0 <= self.step_up_tax_rate <= 1):
131
+ raise BalanceSheetError("the step-up tax rate must be between 0 and 1")
132
+
133
+ @property
134
+ def deferred_tax_liability(self) -> Money:
135
+ """Tax owed in future on the book/tax difference the step-up creates."""
136
+ return self.step_up * self.step_up_tax_rate
137
+
138
+
139
+ @dataclass(frozen=True, slots=True)
140
+ class OpeningBalanceSheet:
141
+ """The pro forma balance sheet at close.
142
+
143
+ Built by :meth:`recapitalise` rather than by hand, so every figure traces to
144
+ the transaction that produced it.
145
+ """
146
+
147
+ cash: Money
148
+ identifiable_assets: Money
149
+ goodwill: Money
150
+ deferred_financing_costs: Money
151
+ unamortised_issue_discount: Money
152
+
153
+ debt_at_face: Money
154
+ operating_liabilities: Money
155
+ deferred_tax_liability: Money
156
+
157
+ sponsor_equity: Money
158
+ rollover_equity: Money
159
+ expensed_at_close: Money
160
+
161
+ def __post_init__(self) -> None:
162
+ difference = self.total_assets - self.total_liabilities_and_equity
163
+ if difference != 0:
164
+ raise UnbalancedBalanceSheetError(
165
+ f"assets ({self.total_assets}) do not equal liabilities and equity "
166
+ f"({self.total_liabilities_and_equity}); the difference is {difference}"
167
+ )
168
+
169
+ # -- Assets ----------------------------------------------------------
170
+
171
+ @property
172
+ def total_assets(self) -> Money:
173
+ return (
174
+ self.cash
175
+ + self.identifiable_assets
176
+ + self.goodwill
177
+ + self.deferred_financing_costs
178
+ + self.unamortised_issue_discount
179
+ )
180
+
181
+ # -- Liabilities and equity ------------------------------------------
182
+
183
+ @property
184
+ def total_liabilities(self) -> Money:
185
+ return self.debt_at_face + self.operating_liabilities + self.deferred_tax_liability
186
+
187
+ @property
188
+ def total_equity(self) -> Money:
189
+ """Equity funded at close, less what was spent and never capitalised.
190
+
191
+ Transaction fees and any other closing payment that buys nothing are
192
+ charged here. They are cash out of the door on day one against which no
193
+ asset stands, so the equity the sponsor owns is worth less than the
194
+ cheque it wrote from the moment the deal signs.
195
+ """
196
+ return self.sponsor_equity + self.rollover_equity - self.expensed_at_close
197
+
198
+ @property
199
+ def total_liabilities_and_equity(self) -> Money:
200
+ return self.total_liabilities + self.total_equity
201
+
202
+ # -- Read-across ------------------------------------------------------
203
+
204
+ @property
205
+ def net_debt(self) -> Money:
206
+ return self.debt_at_face - self.cash
207
+
208
+ @property
209
+ def goodwill_share_of_assets(self) -> Money:
210
+ """Goodwill as a share of the opening asset base.
211
+
212
+ A high number is not a defect, but it is the part of the balance sheet
213
+ that is worth what the next buyer says it is worth, so it is the first
214
+ thing an impairment test comes for.
215
+ """
216
+ if self.total_assets == 0:
217
+ return ZERO
218
+ return self.goodwill / self.total_assets
219
+
220
+ @classmethod
221
+ def recapitalise(
222
+ cls,
223
+ transaction: Transaction,
224
+ book: TargetBookBalanceSheet,
225
+ accounting: PurchaseAccounting | None = None,
226
+ ) -> OpeningBalanceSheet:
227
+ """Apply the transaction to the target's book position.
228
+
229
+ The consideration is the cheque to selling shareholders. Existing debt
230
+ is a liability the buyer assumes and repays at close with the proceeds
231
+ of the new structure, so it is netted inside the identifiable net assets
232
+ rather than added to the price.
233
+ """
234
+ allocation = accounting or PurchaseAccounting()
235
+ valuation = transaction.valuation
236
+
237
+ if book.total_assets < valuation.existing_cash:
238
+ raise BalanceSheetError(
239
+ f"the book balance sheet holds {book.total_assets} of assets but the "
240
+ f"valuation was struck on {valuation.existing_cash} of cash, which is more"
241
+ )
242
+ if book.total_liabilities < valuation.existing_debt:
243
+ raise BalanceSheetError(
244
+ f"the book balance sheet carries {book.total_liabilities} of liabilities "
245
+ f"but the valuation was struck on {valuation.existing_debt} of debt, which "
246
+ f"is more"
247
+ )
248
+
249
+ non_cash_assets = book.total_assets - valuation.existing_cash
250
+ if book.goodwill > non_cash_assets:
251
+ raise BalanceSheetError(
252
+ f"goodwill ({book.goodwill}) exceeds the non-cash assets it sits inside "
253
+ f"({non_cash_assets})"
254
+ )
255
+ operating_liabilities = book.total_liabilities - valuation.existing_debt
256
+
257
+ dtl = allocation.deferred_tax_liability
258
+ identifiable = non_cash_assets - book.goodwill + allocation.step_up
259
+ capitalised_closing_costs = sum(
260
+ (item.amount for item in transaction.other_uses if item.capitalised), ZERO
261
+ )
262
+ expensed_closing_costs = sum(
263
+ (item.amount for item in transaction.other_uses if not item.capitalised), ZERO
264
+ )
265
+
266
+ # Fair value of the identifiable net assets acquired, with the existing
267
+ # debt netted off because the buyer takes it on and clears it at close.
268
+ identifiable_net_assets = (
269
+ valuation.existing_cash
270
+ + identifiable
271
+ - operating_liabilities
272
+ - valuation.existing_debt
273
+ - dtl
274
+ )
275
+ goodwill = valuation.equity_purchase_price - identifiable_net_assets
276
+
277
+ cash = (
278
+ valuation.existing_cash
279
+ - transaction.cash_from_balance_sheet
280
+ + transaction.cash_to_balance_sheet
281
+ )
282
+
283
+ return cls(
284
+ cash=cash,
285
+ identifiable_assets=identifiable + capitalised_closing_costs,
286
+ goodwill=goodwill,
287
+ deferred_financing_costs=transaction.financing_fees,
288
+ unamortised_issue_discount=transaction.original_issue_discount,
289
+ debt_at_face=transaction.total_debt,
290
+ operating_liabilities=operating_liabilities,
291
+ deferred_tax_liability=dtl,
292
+ sponsor_equity=transaction.sponsor_equity,
293
+ rollover_equity=transaction.rollover_equity,
294
+ expensed_at_close=transaction.transaction_fees + expensed_closing_costs,
295
+ )