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/transaction.py
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
"""The transaction: what is bought, and what pays for it.
|
|
2
|
+
|
|
3
|
+
A sources and uses table is the first page of every deal model. Uses are
|
|
4
|
+
everything cash goes towards at close; sources are everything cash comes from.
|
|
5
|
+
The two sides are equal by construction, and the sponsor's equity cheque is
|
|
6
|
+
whatever number makes them equal.
|
|
7
|
+
|
|
8
|
+
The equality is exact here, not approximate. A table that balances to within a
|
|
9
|
+
few thousand on a billion-dollar deal has not nearly balanced — it has a missing
|
|
10
|
+
line item, and making that impossible to overlook is most of what the table is
|
|
11
|
+
for.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from enum import Enum
|
|
18
|
+
|
|
19
|
+
from .money import ONE, ZERO, Money, Numeric, money, safe_div
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"DebtFunding",
|
|
23
|
+
"EntryValuation",
|
|
24
|
+
"LineItem",
|
|
25
|
+
"SourcesAndUses",
|
|
26
|
+
"Transaction",
|
|
27
|
+
"UnbalancedTransactionError",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class UnbalancedTransactionError(ValueError):
|
|
32
|
+
"""Sources and uses do not agree."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class LineKind(Enum):
|
|
36
|
+
SOURCE = "source"
|
|
37
|
+
USE = "use"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True, slots=True)
|
|
41
|
+
class LineItem:
|
|
42
|
+
"""One row of the funding table.
|
|
43
|
+
|
|
44
|
+
``capitalised`` says what a payment leaves behind. Most closing payments buy
|
|
45
|
+
nothing — a change-of-control payment to management settles a contract and
|
|
46
|
+
is gone — and those are charged against equity on day one. A few buy
|
|
47
|
+
something the balance sheet keeps, such as a licence acquired alongside the
|
|
48
|
+
business. The funding table does not care either way, but the opening
|
|
49
|
+
balance sheet cannot be built without knowing.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
label: str
|
|
53
|
+
amount: Money
|
|
54
|
+
note: str = ""
|
|
55
|
+
capitalised: bool = False
|
|
56
|
+
|
|
57
|
+
def __post_init__(self) -> None:
|
|
58
|
+
if not self.label.strip():
|
|
59
|
+
raise ValueError("a line item needs a label")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True, slots=True)
|
|
63
|
+
class EntryValuation:
|
|
64
|
+
"""What the business is worth going in, and what the equity in it costs.
|
|
65
|
+
|
|
66
|
+
The multiple is applied to last-twelve-months EBITDA to give enterprise
|
|
67
|
+
value. Getting from there to the cheque written to selling shareholders
|
|
68
|
+
means undoing the target's own capital structure: repay what it owes, keep
|
|
69
|
+
what it holds.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
ltm_ebitda: Money
|
|
73
|
+
entry_multiple: Money
|
|
74
|
+
existing_debt: Money = ZERO
|
|
75
|
+
existing_cash: Money = ZERO
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def of(
|
|
79
|
+
cls,
|
|
80
|
+
ltm_ebitda: Numeric,
|
|
81
|
+
entry_multiple: Numeric,
|
|
82
|
+
*,
|
|
83
|
+
existing_debt: Numeric = 0,
|
|
84
|
+
existing_cash: Numeric = 0,
|
|
85
|
+
) -> EntryValuation:
|
|
86
|
+
return cls(
|
|
87
|
+
ltm_ebitda=money(ltm_ebitda),
|
|
88
|
+
entry_multiple=money(entry_multiple),
|
|
89
|
+
existing_debt=money(existing_debt),
|
|
90
|
+
existing_cash=money(existing_cash),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def __post_init__(self) -> None:
|
|
94
|
+
if self.ltm_ebitda <= 0:
|
|
95
|
+
raise ValueError("a buyout needs positive EBITDA to price off")
|
|
96
|
+
if self.entry_multiple <= 0:
|
|
97
|
+
raise ValueError("the entry multiple must be positive")
|
|
98
|
+
if self.existing_debt < 0:
|
|
99
|
+
raise ValueError("existing debt must not be negative")
|
|
100
|
+
if self.existing_cash < 0:
|
|
101
|
+
raise ValueError("existing cash must not be negative")
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def enterprise_value(self) -> Money:
|
|
105
|
+
return self.ltm_ebitda * self.entry_multiple
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def net_debt(self) -> Money:
|
|
109
|
+
return self.existing_debt - self.existing_cash
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def equity_purchase_price(self) -> Money:
|
|
113
|
+
"""What selling shareholders receive.
|
|
114
|
+
|
|
115
|
+
Enterprise value less what the business owes, plus what it holds. This
|
|
116
|
+
can be negative for a business bought at a low multiple with more debt
|
|
117
|
+
than the enterprise is worth — a distressed situation, and one the model
|
|
118
|
+
should be able to express rather than reject.
|
|
119
|
+
"""
|
|
120
|
+
return self.enterprise_value - self.net_debt
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass(frozen=True, slots=True)
|
|
124
|
+
class DebtFunding:
|
|
125
|
+
"""A tranche as it appears in the funding table.
|
|
126
|
+
|
|
127
|
+
``issue_price`` is expressed per unit of face: 1 is par, 0.99 is ninety-nine
|
|
128
|
+
cents on the dollar. A tranche placed below par raises less than it carries,
|
|
129
|
+
and the shortfall — original issue discount — is a use of funds. Modelling
|
|
130
|
+
the tranche at proceeds instead of face is the common mistake: it
|
|
131
|
+
understates leverage and understates every interest payment that follows,
|
|
132
|
+
because interest accrues on what is owed rather than on what was received.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
name: str
|
|
136
|
+
face: Money
|
|
137
|
+
issue_price: Money = ONE
|
|
138
|
+
financing_fee_rate: Money = ZERO
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def of(
|
|
142
|
+
cls,
|
|
143
|
+
name: str,
|
|
144
|
+
face: Numeric,
|
|
145
|
+
*,
|
|
146
|
+
issue_price: Numeric = 1,
|
|
147
|
+
financing_fee_rate: Numeric = 0,
|
|
148
|
+
) -> DebtFunding:
|
|
149
|
+
return cls(
|
|
150
|
+
name=name,
|
|
151
|
+
face=money(face),
|
|
152
|
+
issue_price=money(issue_price),
|
|
153
|
+
financing_fee_rate=money(financing_fee_rate),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def __post_init__(self) -> None:
|
|
157
|
+
if not self.name.strip():
|
|
158
|
+
raise ValueError("a tranche needs a name")
|
|
159
|
+
if self.face < 0:
|
|
160
|
+
raise ValueError(f"{self.name}: face amount must not be negative")
|
|
161
|
+
if not (0 < self.issue_price <= 2):
|
|
162
|
+
raise ValueError(
|
|
163
|
+
f"{self.name}: issue price is per unit of face, so 0.99 rather than 99"
|
|
164
|
+
)
|
|
165
|
+
if self.financing_fee_rate < 0:
|
|
166
|
+
raise ValueError(f"{self.name}: financing fee rate must not be negative")
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def proceeds(self) -> Money:
|
|
170
|
+
"""Cash actually raised."""
|
|
171
|
+
return self.face * self.issue_price
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def original_issue_discount(self) -> Money:
|
|
175
|
+
"""Face less proceeds. Negative for paper placed above par."""
|
|
176
|
+
return self.face - self.proceeds
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def financing_fee(self) -> Money:
|
|
180
|
+
"""Arrangement and underwriting fees, charged on face."""
|
|
181
|
+
return self.face * self.financing_fee_rate
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@dataclass(frozen=True, slots=True)
|
|
185
|
+
class SourcesAndUses:
|
|
186
|
+
"""A funding table that is equal on both sides by construction."""
|
|
187
|
+
|
|
188
|
+
sources: tuple[LineItem, ...]
|
|
189
|
+
uses: tuple[LineItem, ...]
|
|
190
|
+
|
|
191
|
+
def __post_init__(self) -> None:
|
|
192
|
+
if self.total_sources != self.total_uses:
|
|
193
|
+
raise UnbalancedTransactionError(
|
|
194
|
+
f"sources ({self.total_sources}) do not equal uses ({self.total_uses}); "
|
|
195
|
+
f"the difference is {self.total_sources - self.total_uses}"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def total_sources(self) -> Money:
|
|
200
|
+
return sum((item.amount for item in self.sources), ZERO)
|
|
201
|
+
|
|
202
|
+
@property
|
|
203
|
+
def total_uses(self) -> Money:
|
|
204
|
+
return sum((item.amount for item in self.uses), ZERO)
|
|
205
|
+
|
|
206
|
+
def source(self, label: str) -> Money:
|
|
207
|
+
"""Amount on the sources side under ``label``, zero if absent."""
|
|
208
|
+
return sum((i.amount for i in self.sources if i.label == label), ZERO)
|
|
209
|
+
|
|
210
|
+
def use(self, label: str) -> Money:
|
|
211
|
+
"""Amount on the uses side under ``label``, zero if absent."""
|
|
212
|
+
return sum((i.amount for i in self.uses if i.label == label), ZERO)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
#: Labels used on the funding table, named once so lookups cannot drift.
|
|
216
|
+
SPONSOR_EQUITY = "Sponsor equity"
|
|
217
|
+
ROLLOVER_EQUITY = "Rollover equity"
|
|
218
|
+
CASH_FROM_BALANCE_SHEET = "Cash from balance sheet"
|
|
219
|
+
EQUITY_PURCHASE_PRICE = "Purchase of equity"
|
|
220
|
+
REFINANCE_EXISTING_DEBT = "Repay existing debt"
|
|
221
|
+
TRANSACTION_FEES = "Transaction fees"
|
|
222
|
+
FINANCING_FEES = "Financing fees"
|
|
223
|
+
ORIGINAL_ISSUE_DISCOUNT = "Original issue discount"
|
|
224
|
+
CASH_TO_BALANCE_SHEET = "Cash to balance sheet"
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@dataclass(frozen=True, slots=True)
|
|
228
|
+
class Transaction:
|
|
229
|
+
"""A complete purchase: what is bought, how it is funded, what it costs.
|
|
230
|
+
|
|
231
|
+
The sponsor's cheque is not an input. It is derived as the residual that
|
|
232
|
+
makes the table balance, which is how it works on a real deal: leverage is
|
|
233
|
+
negotiated with lenders, the price is negotiated with the seller, and equity
|
|
234
|
+
fills whatever gap is left.
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
valuation: EntryValuation
|
|
238
|
+
debt: tuple[DebtFunding, ...] = ()
|
|
239
|
+
rollover_equity: Money = ZERO
|
|
240
|
+
cash_from_balance_sheet: Money = ZERO
|
|
241
|
+
cash_to_balance_sheet: Money = ZERO
|
|
242
|
+
transaction_fee_rate: Money = ZERO
|
|
243
|
+
other_uses: tuple[LineItem, ...] = field(default_factory=tuple)
|
|
244
|
+
|
|
245
|
+
@classmethod
|
|
246
|
+
def of(
|
|
247
|
+
cls,
|
|
248
|
+
valuation: EntryValuation,
|
|
249
|
+
*,
|
|
250
|
+
debt: tuple[DebtFunding, ...] = (),
|
|
251
|
+
rollover_equity: Numeric = 0,
|
|
252
|
+
cash_from_balance_sheet: Numeric = 0,
|
|
253
|
+
cash_to_balance_sheet: Numeric = 0,
|
|
254
|
+
transaction_fee_rate: Numeric = 0,
|
|
255
|
+
other_uses: tuple[LineItem, ...] = (),
|
|
256
|
+
) -> Transaction:
|
|
257
|
+
return cls(
|
|
258
|
+
valuation=valuation,
|
|
259
|
+
debt=debt,
|
|
260
|
+
rollover_equity=money(rollover_equity),
|
|
261
|
+
cash_from_balance_sheet=money(cash_from_balance_sheet),
|
|
262
|
+
cash_to_balance_sheet=money(cash_to_balance_sheet),
|
|
263
|
+
transaction_fee_rate=money(transaction_fee_rate),
|
|
264
|
+
other_uses=other_uses,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
def __post_init__(self) -> None:
|
|
268
|
+
if self.rollover_equity < 0:
|
|
269
|
+
raise ValueError("rollover equity must not be negative")
|
|
270
|
+
if self.cash_from_balance_sheet < 0:
|
|
271
|
+
raise ValueError("cash taken from the balance sheet must not be negative")
|
|
272
|
+
if self.cash_to_balance_sheet < 0:
|
|
273
|
+
raise ValueError("cash funded to the balance sheet must not be negative")
|
|
274
|
+
if self.transaction_fee_rate < 0:
|
|
275
|
+
raise ValueError("the transaction fee rate must not be negative")
|
|
276
|
+
if self.cash_from_balance_sheet > self.valuation.existing_cash:
|
|
277
|
+
raise ValueError(
|
|
278
|
+
f"cannot take {self.cash_from_balance_sheet} from the balance sheet; "
|
|
279
|
+
f"the target holds {self.valuation.existing_cash}"
|
|
280
|
+
)
|
|
281
|
+
names = [t.name for t in self.debt]
|
|
282
|
+
if len(names) != len(set(names)):
|
|
283
|
+
raise ValueError("tranche names must be distinct")
|
|
284
|
+
|
|
285
|
+
# -- Derived amounts -------------------------------------------------
|
|
286
|
+
|
|
287
|
+
@property
|
|
288
|
+
def total_debt(self) -> Money:
|
|
289
|
+
"""Face value of debt raised. This is what accrues interest."""
|
|
290
|
+
return sum((t.face for t in self.debt), ZERO)
|
|
291
|
+
|
|
292
|
+
@property
|
|
293
|
+
def debt_proceeds(self) -> Money:
|
|
294
|
+
"""Cash raised from the debt, after issue discount."""
|
|
295
|
+
return sum((t.proceeds for t in self.debt), ZERO)
|
|
296
|
+
|
|
297
|
+
@property
|
|
298
|
+
def original_issue_discount(self) -> Money:
|
|
299
|
+
return sum((t.original_issue_discount for t in self.debt), ZERO)
|
|
300
|
+
|
|
301
|
+
@property
|
|
302
|
+
def financing_fees(self) -> Money:
|
|
303
|
+
"""Capitalised at close and amortised over the life of the paper."""
|
|
304
|
+
return sum((t.financing_fee for t in self.debt), ZERO)
|
|
305
|
+
|
|
306
|
+
@property
|
|
307
|
+
def transaction_fees(self) -> Money:
|
|
308
|
+
"""Advisory, legal and diligence costs, charged on enterprise value."""
|
|
309
|
+
return self.valuation.enterprise_value * self.transaction_fee_rate
|
|
310
|
+
|
|
311
|
+
@property
|
|
312
|
+
def total_fees(self) -> Money:
|
|
313
|
+
return self.transaction_fees + self.financing_fees
|
|
314
|
+
|
|
315
|
+
@property
|
|
316
|
+
def sponsor_equity(self) -> Money:
|
|
317
|
+
"""The plug.
|
|
318
|
+
|
|
319
|
+
Everything the deal needs, less everything other than the sponsor that
|
|
320
|
+
pays for it. A negative result means the debt raised more than the deal
|
|
321
|
+
required and the excess goes out to the sponsor at close.
|
|
322
|
+
"""
|
|
323
|
+
return self._gross_uses - self._non_sponsor_sources
|
|
324
|
+
|
|
325
|
+
@property
|
|
326
|
+
def is_overfunded(self) -> bool:
|
|
327
|
+
"""Whether the structure returns cash to the sponsor at close.
|
|
328
|
+
|
|
329
|
+
Legal and not unheard of, but it means the deal is being financed
|
|
330
|
+
entirely by its own borrowing capacity, so it is worth naming.
|
|
331
|
+
"""
|
|
332
|
+
return self.sponsor_equity < 0
|
|
333
|
+
|
|
334
|
+
@property
|
|
335
|
+
def _gross_uses(self) -> Money:
|
|
336
|
+
return (
|
|
337
|
+
self.valuation.equity_purchase_price
|
|
338
|
+
+ self.valuation.existing_debt
|
|
339
|
+
+ self.transaction_fees
|
|
340
|
+
+ self.financing_fees
|
|
341
|
+
+ self.original_issue_discount
|
|
342
|
+
+ self.cash_to_balance_sheet
|
|
343
|
+
+ sum((item.amount for item in self.other_uses), ZERO)
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
@property
|
|
347
|
+
def _non_sponsor_sources(self) -> Money:
|
|
348
|
+
return self.total_debt + self.rollover_equity + self.cash_from_balance_sheet
|
|
349
|
+
|
|
350
|
+
# -- The table -------------------------------------------------------
|
|
351
|
+
|
|
352
|
+
def sources_and_uses(self) -> SourcesAndUses:
|
|
353
|
+
"""Build the funding table.
|
|
354
|
+
|
|
355
|
+
Debt appears on the sources side at face rather than at proceeds, with
|
|
356
|
+
the discount carried across to uses. Both presentations balance, but
|
|
357
|
+
this one keeps face value visible, and face value is what leverage and
|
|
358
|
+
interest are computed on.
|
|
359
|
+
"""
|
|
360
|
+
sources: list[LineItem] = [
|
|
361
|
+
LineItem(t.name, t.face, note=_issue_note(t)) for t in self.debt if t.face
|
|
362
|
+
]
|
|
363
|
+
if self.rollover_equity:
|
|
364
|
+
sources.append(
|
|
365
|
+
LineItem(ROLLOVER_EQUITY, self.rollover_equity, note="no cash moves")
|
|
366
|
+
)
|
|
367
|
+
if self.cash_from_balance_sheet:
|
|
368
|
+
sources.append(LineItem(CASH_FROM_BALANCE_SHEET, self.cash_from_balance_sheet))
|
|
369
|
+
sources.append(
|
|
370
|
+
LineItem(
|
|
371
|
+
SPONSOR_EQUITY,
|
|
372
|
+
self.sponsor_equity,
|
|
373
|
+
note="distribution to the sponsor at close" if self.is_overfunded else "the plug",
|
|
374
|
+
)
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
uses: list[LineItem] = [
|
|
378
|
+
LineItem(EQUITY_PURCHASE_PRICE, self.valuation.equity_purchase_price)
|
|
379
|
+
]
|
|
380
|
+
if self.valuation.existing_debt:
|
|
381
|
+
uses.append(LineItem(REFINANCE_EXISTING_DEBT, self.valuation.existing_debt))
|
|
382
|
+
if self.transaction_fees:
|
|
383
|
+
uses.append(LineItem(TRANSACTION_FEES, self.transaction_fees, note="expensed"))
|
|
384
|
+
if self.financing_fees:
|
|
385
|
+
uses.append(LineItem(FINANCING_FEES, self.financing_fees, note="capitalised"))
|
|
386
|
+
if self.original_issue_discount:
|
|
387
|
+
uses.append(LineItem(ORIGINAL_ISSUE_DISCOUNT, self.original_issue_discount))
|
|
388
|
+
if self.cash_to_balance_sheet:
|
|
389
|
+
uses.append(LineItem(CASH_TO_BALANCE_SHEET, self.cash_to_balance_sheet))
|
|
390
|
+
uses.extend(self.other_uses)
|
|
391
|
+
|
|
392
|
+
return SourcesAndUses(sources=tuple(sources), uses=tuple(uses))
|
|
393
|
+
|
|
394
|
+
# -- Entry metrics ---------------------------------------------------
|
|
395
|
+
|
|
396
|
+
@property
|
|
397
|
+
def entry_leverage(self) -> Money:
|
|
398
|
+
"""Total debt as a multiple of LTM EBITDA."""
|
|
399
|
+
return self.total_debt / self.valuation.ltm_ebitda
|
|
400
|
+
|
|
401
|
+
@property
|
|
402
|
+
def total_capitalisation(self) -> Money:
|
|
403
|
+
"""Debt plus every form of equity that funds the deal."""
|
|
404
|
+
return self.total_debt + self.sponsor_equity + self.rollover_equity
|
|
405
|
+
|
|
406
|
+
@property
|
|
407
|
+
def equity_contribution_rate(self) -> Money:
|
|
408
|
+
"""Equity as a share of total capitalisation.
|
|
409
|
+
|
|
410
|
+
The number a lender looks at first. Sponsors are expected to have real
|
|
411
|
+
money at risk, and a thin contribution is what a credit committee
|
|
412
|
+
objects to before it objects to anything else.
|
|
413
|
+
"""
|
|
414
|
+
return safe_div(
|
|
415
|
+
self.sponsor_equity + self.rollover_equity,
|
|
416
|
+
self.total_capitalisation,
|
|
417
|
+
default=ZERO,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
@property
|
|
421
|
+
def sponsor_ownership(self) -> Money:
|
|
422
|
+
"""The sponsor's share of the equity, with rollover holders taking the rest."""
|
|
423
|
+
total_equity = self.sponsor_equity + self.rollover_equity
|
|
424
|
+
return safe_div(self.sponsor_equity, total_equity, default=ZERO)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _issue_note(tranche: DebtFunding) -> str:
|
|
428
|
+
if tranche.issue_price == ONE:
|
|
429
|
+
return ""
|
|
430
|
+
return f"issued at {tranche.issue_price * 100:.2f}"
|