nmtc-calc 0.1.0__tar.gz
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.
- nmtc_calc-0.1.0/PKG-INFO +139 -0
- nmtc_calc-0.1.0/README.md +128 -0
- nmtc_calc-0.1.0/nmtc_calc.egg-info/PKG-INFO +139 -0
- nmtc_calc-0.1.0/nmtc_calc.egg-info/SOURCES.txt +21 -0
- nmtc_calc-0.1.0/nmtc_calc.egg-info/dependency_links.txt +1 -0
- nmtc_calc-0.1.0/nmtc_calc.egg-info/requires.txt +2 -0
- nmtc_calc-0.1.0/nmtc_calc.egg-info/top_level.txt +1 -0
- nmtc_calc-0.1.0/nmtccalc/__init__.py +5 -0
- nmtc_calc-0.1.0/nmtccalc/data/__init__.py +0 -0
- nmtc_calc-0.1.0/nmtccalc/data/schema.py +95 -0
- nmtc_calc-0.1.0/nmtccalc/models/__init__.py +0 -0
- nmtc_calc-0.1.0/nmtccalc/models/credits.py +91 -0
- nmtc_calc-0.1.0/nmtccalc/models/investor.py +109 -0
- nmtc_calc-0.1.0/nmtccalc/models/subsidy.py +98 -0
- nmtc_calc-0.1.0/nmtccalc/models/transaction.py +101 -0
- nmtc_calc-0.1.0/nmtccalc/utils/__init__.py +0 -0
- nmtc_calc-0.1.0/pyproject.toml +18 -0
- nmtc_calc-0.1.0/setup.cfg +4 -0
- nmtc_calc-0.1.0/setup.py +11 -0
- nmtc_calc-0.1.0/tests/test_credits.py +49 -0
- nmtc_calc-0.1.0/tests/test_investor.py +45 -0
- nmtc_calc-0.1.0/tests/test_subsidy.py +42 -0
- nmtc_calc-0.1.0/tests/test_transaction.py +68 -0
nmtc_calc-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nmtc-calc
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python calculator for New Markets Tax Credit (NMTC) leveraged transactions
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/Jaypatel1511/nmtc-calc
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: pandas>=1.4.0
|
|
10
|
+
Requires-Dist: numpy>=1.21.0
|
|
11
|
+
|
|
12
|
+
# nmtc-calc 🏗️
|
|
13
|
+
|
|
14
|
+
**Python calculator for New Markets Tax Credit (NMTC) leveraged transactions.**
|
|
15
|
+
|
|
16
|
+
Built for CDFI practitioners, CDEs, tax credit investors, and project sponsors who need
|
|
17
|
+
reproducible, auditable NMTC deal math — without starting from scratch in Excel.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Why nmtc-calc?
|
|
22
|
+
|
|
23
|
+
New Markets Tax Credit transactions involve complex layered capital structures — QEIs,
|
|
24
|
+
QLICIs, leverage loans, 7-year credit schedules, investor IRR, and net subsidy calculations.
|
|
25
|
+
Every practitioner builds these models from scratch in Excel. `nmtc-calc` standardizes
|
|
26
|
+
and automates the math.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
cat > README.md << 'EOF'
|
|
34
|
+
# nmtc-calc 🏗️
|
|
35
|
+
|
|
36
|
+
**Python calculator for New Markets Tax Credit (NMTC) leveraged transactions.**
|
|
37
|
+
|
|
38
|
+
Built for CDFI practitioners, CDEs, tax credit investors, and project sponsors who need
|
|
39
|
+
reproducible, auditable NMTC deal math — without starting from scratch in Excel.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Why nmtc-calc?
|
|
44
|
+
|
|
45
|
+
New Markets Tax Credit transactions involve complex layered capital structures — QEIs,
|
|
46
|
+
QLICIs, leverage loans, 7-year credit schedules, investor IRR, and net subsidy calculations.
|
|
47
|
+
Every practitioner builds these models from scratch in Excel. `nmtc-calc` standardizes
|
|
48
|
+
and automates the math.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install nmtc-calc
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Quickstart
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from nmtccalc import NMTCDeal, transaction, credits, investor, subsidy
|
|
64
|
+
|
|
65
|
+
deal = NMTCDeal(
|
|
66
|
+
project_name="Southside Community Health Center",
|
|
67
|
+
total_project_cost=10_000_000,
|
|
68
|
+
nmtc_allocation=10_000_000,
|
|
69
|
+
credit_price=0.83,
|
|
70
|
+
leverage_loan_rate=0.045,
|
|
71
|
+
qlici_a_loan_rate=0.045,
|
|
72
|
+
qlici_b_loan_rate=0.010,
|
|
73
|
+
cde_fee_rate=0.02,
|
|
74
|
+
compliance_years=7,
|
|
75
|
+
discount_rate=0.08,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Full capital stack
|
|
79
|
+
transaction.structure(deal).summary()
|
|
80
|
+
|
|
81
|
+
# 7-year credit schedule
|
|
82
|
+
credits.schedule(deal).summary()
|
|
83
|
+
|
|
84
|
+
# Investor economics & IRR
|
|
85
|
+
investor.analyze(deal).summary()
|
|
86
|
+
|
|
87
|
+
# Net subsidy to project
|
|
88
|
+
subsidy.analyze(deal).summary()
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Modules
|
|
94
|
+
|
|
95
|
+
| Module | What It Computes |
|
|
96
|
+
|--------|-----------------|
|
|
97
|
+
| `transaction` | QEI, NMTCs, investor equity, leverage loan, QLICI A/B split |
|
|
98
|
+
| `credits` | 7-year credit schedule (5%/5%/5%/6%/6%/6%/6%), PV of credits |
|
|
99
|
+
| `investor` | Investor IRR, MOIC, gross/net benefit |
|
|
100
|
+
| `subsidy` | Net subsidy to QALICB, effective cost of capital, interest savings |
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Key NMTC Concepts
|
|
105
|
+
|
|
106
|
+
| Term | Definition |
|
|
107
|
+
|------|-----------|
|
|
108
|
+
| QEI | Qualified Equity Investment — the total investment made into the CDE |
|
|
109
|
+
| QLICI | Qualified Low-Income Community Investment — loans from CDE to QALICB |
|
|
110
|
+
| QALICB | Qualified Active Low-Income Community Business — the project borrower |
|
|
111
|
+
| CDE | Community Development Entity — allocatee of NMTC authority |
|
|
112
|
+
| A Loan | Senior QLICI mirroring the leverage loan |
|
|
113
|
+
| B Loan | Subordinate QLICI mirroring investor equity, typically forgiven at year 7 |
|
|
114
|
+
| Credit Price | $ per $1 of NMTC benefit paid by investor (typically $0.70–$0.85) |
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Running Tests
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
PYTHONPATH=. pytest tests/ -v
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
32 tests across all modules.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Who This Is For
|
|
129
|
+
|
|
130
|
+
- **CDEs** structuring NMTC allocations for projects
|
|
131
|
+
- **Tax credit investors** evaluating deal economics
|
|
132
|
+
- **Project sponsors** understanding subsidy and cost of capital
|
|
133
|
+
- **CDFI analysts** modeling NMTC transactions in IC memos
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
MIT © 2026 Jaypatel1511
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# nmtc-calc 🏗️
|
|
2
|
+
|
|
3
|
+
**Python calculator for New Markets Tax Credit (NMTC) leveraged transactions.**
|
|
4
|
+
|
|
5
|
+
Built for CDFI practitioners, CDEs, tax credit investors, and project sponsors who need
|
|
6
|
+
reproducible, auditable NMTC deal math — without starting from scratch in Excel.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Why nmtc-calc?
|
|
11
|
+
|
|
12
|
+
New Markets Tax Credit transactions involve complex layered capital structures — QEIs,
|
|
13
|
+
QLICIs, leverage loans, 7-year credit schedules, investor IRR, and net subsidy calculations.
|
|
14
|
+
Every practitioner builds these models from scratch in Excel. `nmtc-calc` standardizes
|
|
15
|
+
and automates the math.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
cat > README.md << 'EOF'
|
|
23
|
+
# nmtc-calc 🏗️
|
|
24
|
+
|
|
25
|
+
**Python calculator for New Markets Tax Credit (NMTC) leveraged transactions.**
|
|
26
|
+
|
|
27
|
+
Built for CDFI practitioners, CDEs, tax credit investors, and project sponsors who need
|
|
28
|
+
reproducible, auditable NMTC deal math — without starting from scratch in Excel.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Why nmtc-calc?
|
|
33
|
+
|
|
34
|
+
New Markets Tax Credit transactions involve complex layered capital structures — QEIs,
|
|
35
|
+
QLICIs, leverage loans, 7-year credit schedules, investor IRR, and net subsidy calculations.
|
|
36
|
+
Every practitioner builds these models from scratch in Excel. `nmtc-calc` standardizes
|
|
37
|
+
and automates the math.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install nmtc-calc
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Quickstart
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from nmtccalc import NMTCDeal, transaction, credits, investor, subsidy
|
|
53
|
+
|
|
54
|
+
deal = NMTCDeal(
|
|
55
|
+
project_name="Southside Community Health Center",
|
|
56
|
+
total_project_cost=10_000_000,
|
|
57
|
+
nmtc_allocation=10_000_000,
|
|
58
|
+
credit_price=0.83,
|
|
59
|
+
leverage_loan_rate=0.045,
|
|
60
|
+
qlici_a_loan_rate=0.045,
|
|
61
|
+
qlici_b_loan_rate=0.010,
|
|
62
|
+
cde_fee_rate=0.02,
|
|
63
|
+
compliance_years=7,
|
|
64
|
+
discount_rate=0.08,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Full capital stack
|
|
68
|
+
transaction.structure(deal).summary()
|
|
69
|
+
|
|
70
|
+
# 7-year credit schedule
|
|
71
|
+
credits.schedule(deal).summary()
|
|
72
|
+
|
|
73
|
+
# Investor economics & IRR
|
|
74
|
+
investor.analyze(deal).summary()
|
|
75
|
+
|
|
76
|
+
# Net subsidy to project
|
|
77
|
+
subsidy.analyze(deal).summary()
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Modules
|
|
83
|
+
|
|
84
|
+
| Module | What It Computes |
|
|
85
|
+
|--------|-----------------|
|
|
86
|
+
| `transaction` | QEI, NMTCs, investor equity, leverage loan, QLICI A/B split |
|
|
87
|
+
| `credits` | 7-year credit schedule (5%/5%/5%/6%/6%/6%/6%), PV of credits |
|
|
88
|
+
| `investor` | Investor IRR, MOIC, gross/net benefit |
|
|
89
|
+
| `subsidy` | Net subsidy to QALICB, effective cost of capital, interest savings |
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Key NMTC Concepts
|
|
94
|
+
|
|
95
|
+
| Term | Definition |
|
|
96
|
+
|------|-----------|
|
|
97
|
+
| QEI | Qualified Equity Investment — the total investment made into the CDE |
|
|
98
|
+
| QLICI | Qualified Low-Income Community Investment — loans from CDE to QALICB |
|
|
99
|
+
| QALICB | Qualified Active Low-Income Community Business — the project borrower |
|
|
100
|
+
| CDE | Community Development Entity — allocatee of NMTC authority |
|
|
101
|
+
| A Loan | Senior QLICI mirroring the leverage loan |
|
|
102
|
+
| B Loan | Subordinate QLICI mirroring investor equity, typically forgiven at year 7 |
|
|
103
|
+
| Credit Price | $ per $1 of NMTC benefit paid by investor (typically $0.70–$0.85) |
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Running Tests
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
PYTHONPATH=. pytest tests/ -v
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
32 tests across all modules.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Who This Is For
|
|
118
|
+
|
|
119
|
+
- **CDEs** structuring NMTC allocations for projects
|
|
120
|
+
- **Tax credit investors** evaluating deal economics
|
|
121
|
+
- **Project sponsors** understanding subsidy and cost of capital
|
|
122
|
+
- **CDFI analysts** modeling NMTC transactions in IC memos
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## License
|
|
127
|
+
|
|
128
|
+
MIT © 2026 Jaypatel1511
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nmtc-calc
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python calculator for New Markets Tax Credit (NMTC) leveraged transactions
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/Jaypatel1511/nmtc-calc
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: pandas>=1.4.0
|
|
10
|
+
Requires-Dist: numpy>=1.21.0
|
|
11
|
+
|
|
12
|
+
# nmtc-calc 🏗️
|
|
13
|
+
|
|
14
|
+
**Python calculator for New Markets Tax Credit (NMTC) leveraged transactions.**
|
|
15
|
+
|
|
16
|
+
Built for CDFI practitioners, CDEs, tax credit investors, and project sponsors who need
|
|
17
|
+
reproducible, auditable NMTC deal math — without starting from scratch in Excel.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Why nmtc-calc?
|
|
22
|
+
|
|
23
|
+
New Markets Tax Credit transactions involve complex layered capital structures — QEIs,
|
|
24
|
+
QLICIs, leverage loans, 7-year credit schedules, investor IRR, and net subsidy calculations.
|
|
25
|
+
Every practitioner builds these models from scratch in Excel. `nmtc-calc` standardizes
|
|
26
|
+
and automates the math.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
cat > README.md << 'EOF'
|
|
34
|
+
# nmtc-calc 🏗️
|
|
35
|
+
|
|
36
|
+
**Python calculator for New Markets Tax Credit (NMTC) leveraged transactions.**
|
|
37
|
+
|
|
38
|
+
Built for CDFI practitioners, CDEs, tax credit investors, and project sponsors who need
|
|
39
|
+
reproducible, auditable NMTC deal math — without starting from scratch in Excel.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Why nmtc-calc?
|
|
44
|
+
|
|
45
|
+
New Markets Tax Credit transactions involve complex layered capital structures — QEIs,
|
|
46
|
+
QLICIs, leverage loans, 7-year credit schedules, investor IRR, and net subsidy calculations.
|
|
47
|
+
Every practitioner builds these models from scratch in Excel. `nmtc-calc` standardizes
|
|
48
|
+
and automates the math.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install nmtc-calc
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Quickstart
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from nmtccalc import NMTCDeal, transaction, credits, investor, subsidy
|
|
64
|
+
|
|
65
|
+
deal = NMTCDeal(
|
|
66
|
+
project_name="Southside Community Health Center",
|
|
67
|
+
total_project_cost=10_000_000,
|
|
68
|
+
nmtc_allocation=10_000_000,
|
|
69
|
+
credit_price=0.83,
|
|
70
|
+
leverage_loan_rate=0.045,
|
|
71
|
+
qlici_a_loan_rate=0.045,
|
|
72
|
+
qlici_b_loan_rate=0.010,
|
|
73
|
+
cde_fee_rate=0.02,
|
|
74
|
+
compliance_years=7,
|
|
75
|
+
discount_rate=0.08,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Full capital stack
|
|
79
|
+
transaction.structure(deal).summary()
|
|
80
|
+
|
|
81
|
+
# 7-year credit schedule
|
|
82
|
+
credits.schedule(deal).summary()
|
|
83
|
+
|
|
84
|
+
# Investor economics & IRR
|
|
85
|
+
investor.analyze(deal).summary()
|
|
86
|
+
|
|
87
|
+
# Net subsidy to project
|
|
88
|
+
subsidy.analyze(deal).summary()
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Modules
|
|
94
|
+
|
|
95
|
+
| Module | What It Computes |
|
|
96
|
+
|--------|-----------------|
|
|
97
|
+
| `transaction` | QEI, NMTCs, investor equity, leverage loan, QLICI A/B split |
|
|
98
|
+
| `credits` | 7-year credit schedule (5%/5%/5%/6%/6%/6%/6%), PV of credits |
|
|
99
|
+
| `investor` | Investor IRR, MOIC, gross/net benefit |
|
|
100
|
+
| `subsidy` | Net subsidy to QALICB, effective cost of capital, interest savings |
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Key NMTC Concepts
|
|
105
|
+
|
|
106
|
+
| Term | Definition |
|
|
107
|
+
|------|-----------|
|
|
108
|
+
| QEI | Qualified Equity Investment — the total investment made into the CDE |
|
|
109
|
+
| QLICI | Qualified Low-Income Community Investment — loans from CDE to QALICB |
|
|
110
|
+
| QALICB | Qualified Active Low-Income Community Business — the project borrower |
|
|
111
|
+
| CDE | Community Development Entity — allocatee of NMTC authority |
|
|
112
|
+
| A Loan | Senior QLICI mirroring the leverage loan |
|
|
113
|
+
| B Loan | Subordinate QLICI mirroring investor equity, typically forgiven at year 7 |
|
|
114
|
+
| Credit Price | $ per $1 of NMTC benefit paid by investor (typically $0.70–$0.85) |
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Running Tests
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
PYTHONPATH=. pytest tests/ -v
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
32 tests across all modules.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Who This Is For
|
|
129
|
+
|
|
130
|
+
- **CDEs** structuring NMTC allocations for projects
|
|
131
|
+
- **Tax credit investors** evaluating deal economics
|
|
132
|
+
- **Project sponsors** understanding subsidy and cost of capital
|
|
133
|
+
- **CDFI analysts** modeling NMTC transactions in IC memos
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
MIT © 2026 Jaypatel1511
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
setup.py
|
|
4
|
+
nmtc_calc.egg-info/PKG-INFO
|
|
5
|
+
nmtc_calc.egg-info/SOURCES.txt
|
|
6
|
+
nmtc_calc.egg-info/dependency_links.txt
|
|
7
|
+
nmtc_calc.egg-info/requires.txt
|
|
8
|
+
nmtc_calc.egg-info/top_level.txt
|
|
9
|
+
nmtccalc/__init__.py
|
|
10
|
+
nmtccalc/data/__init__.py
|
|
11
|
+
nmtccalc/data/schema.py
|
|
12
|
+
nmtccalc/models/__init__.py
|
|
13
|
+
nmtccalc/models/credits.py
|
|
14
|
+
nmtccalc/models/investor.py
|
|
15
|
+
nmtccalc/models/subsidy.py
|
|
16
|
+
nmtccalc/models/transaction.py
|
|
17
|
+
nmtccalc/utils/__init__.py
|
|
18
|
+
tests/test_credits.py
|
|
19
|
+
tests/test_investor.py
|
|
20
|
+
tests/test_subsidy.py
|
|
21
|
+
tests/test_transaction.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nmtccalc
|
|
File without changes
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class NMTCDeal:
|
|
7
|
+
"""
|
|
8
|
+
Core input contract for an NMTC leveraged transaction.
|
|
9
|
+
All dollar amounts in whole dollars (e.g. 10_000_000 for $10MM).
|
|
10
|
+
"""
|
|
11
|
+
project_name: str
|
|
12
|
+
total_project_cost: float # total project budget
|
|
13
|
+
nmtc_allocation: float # QEI amount
|
|
14
|
+
credit_price: float # $ per $1 of NMTC benefit e.g. 0.83
|
|
15
|
+
leverage_loan_rate: float # annual interest rate e.g. 0.045
|
|
16
|
+
qlici_a_loan_rate: float # senior QLICI loan rate
|
|
17
|
+
qlici_b_loan_rate: float # subordinate QLICI loan rate
|
|
18
|
+
cde_fee_rate: float # CDE upfront fee as % of QEI e.g. 0.02
|
|
19
|
+
compliance_years: int = 7 # always 7 per Section 45D
|
|
20
|
+
discount_rate: float = 0.08 # for NPV/IRR calculations
|
|
21
|
+
investor_name: Optional[str] = None
|
|
22
|
+
cde_name: Optional[str] = None
|
|
23
|
+
project_location: Optional[str] = None
|
|
24
|
+
|
|
25
|
+
def __post_init__(self):
|
|
26
|
+
if self.total_project_cost <= 0:
|
|
27
|
+
raise ValueError("total_project_cost must be positive")
|
|
28
|
+
if self.nmtc_allocation <= 0:
|
|
29
|
+
raise ValueError("nmtc_allocation (QEI) must be positive")
|
|
30
|
+
if self.nmtc_allocation > self.total_project_cost:
|
|
31
|
+
raise ValueError("nmtc_allocation cannot exceed total_project_cost")
|
|
32
|
+
if not (0 < self.credit_price < 1):
|
|
33
|
+
raise ValueError("credit_price must be between 0 and 1 (e.g. 0.83)")
|
|
34
|
+
if not (0 < self.cde_fee_rate < 1):
|
|
35
|
+
raise ValueError("cde_fee_rate must be between 0 and 1 (e.g. 0.02)")
|
|
36
|
+
if self.compliance_years != 7:
|
|
37
|
+
raise ValueError("compliance_years must be 7 per Section 45D")
|
|
38
|
+
if not (0 < self.discount_rate < 1):
|
|
39
|
+
raise ValueError("discount_rate must be between 0 and 1 (e.g. 0.08)")
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def qei(self) -> float:
|
|
43
|
+
"""Qualified Equity Investment amount."""
|
|
44
|
+
return self.nmtc_allocation
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def total_nmtcs(self) -> float:
|
|
48
|
+
"""Total tax credits generated: 39% of QEI."""
|
|
49
|
+
return self.qei * 0.39
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def investor_equity(self) -> float:
|
|
53
|
+
"""Investor equity contribution: total NMTCs × credit price."""
|
|
54
|
+
return self.total_nmtcs * self.credit_price
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def leverage_loan(self) -> float:
|
|
58
|
+
"""Leverage loan amount: QEI minus investor equity."""
|
|
59
|
+
return self.qei - self.investor_equity
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def cde_fee(self) -> float:
|
|
63
|
+
"""CDE upfront fee in dollars."""
|
|
64
|
+
return self.qei * self.cde_fee_rate
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def qlici_total(self) -> float:
|
|
68
|
+
"""Total QLICI to QALICB: QEI minus CDE fee."""
|
|
69
|
+
return self.qei - self.cde_fee
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def qlici_a_loan(self) -> float:
|
|
73
|
+
"""A Loan: mirrors leverage loan."""
|
|
74
|
+
return self.leverage_loan
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def qlici_b_loan(self) -> float:
|
|
78
|
+
"""B Loan: mirrors investor equity net of CDE fee."""
|
|
79
|
+
return self.investor_equity - self.cde_fee
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def qei_mm(self) -> float:
|
|
83
|
+
return self.qei / 1_000_000
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def total_project_cost_mm(self) -> float:
|
|
87
|
+
return self.total_project_cost / 1_000_000
|
|
88
|
+
|
|
89
|
+
def __repr__(self):
|
|
90
|
+
return (
|
|
91
|
+
f"NMTCDeal(project='{self.project_name}', "
|
|
92
|
+
f"QEI=${self.qei_mm:.1f}MM, "
|
|
93
|
+
f"NMTCs=${self.total_nmtcs/1e6:.2f}MM, "
|
|
94
|
+
f"credit_price=${self.credit_price:.2f})"
|
|
95
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from nmtccalc.data.schema import NMTCDeal
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class CreditScheduleResult:
|
|
10
|
+
"""Output object from NMTC 7-year credit schedule."""
|
|
11
|
+
project_name: str
|
|
12
|
+
qei: float
|
|
13
|
+
total_nmtcs: float
|
|
14
|
+
annual_credits: list
|
|
15
|
+
cumulative_credits: list
|
|
16
|
+
pv_credits: float
|
|
17
|
+
discount_rate: float
|
|
18
|
+
|
|
19
|
+
def summary(self) -> pd.DataFrame:
|
|
20
|
+
rows = []
|
|
21
|
+
for yr, (credit, cumulative) in enumerate(
|
|
22
|
+
zip(self.annual_credits, self.cumulative_credits), 1
|
|
23
|
+
):
|
|
24
|
+
rate = "5%" if yr <= 3 else "6%"
|
|
25
|
+
rows.append({
|
|
26
|
+
"Year": f"Y{yr}",
|
|
27
|
+
"Credit Rate": rate,
|
|
28
|
+
"Annual Credit ($)": f"${credit:,.0f}",
|
|
29
|
+
"Cumulative ($)": f"${cumulative:,.0f}",
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
df = pd.DataFrame(rows)
|
|
33
|
+
print(f"\n7-Year NMTC Credit Schedule — {self.project_name}")
|
|
34
|
+
print(f"QEI: ${self.qei/1e6:.2f}MM | Total NMTCs: ${self.total_nmtcs/1e6:.2f}MM")
|
|
35
|
+
print("-" * 60)
|
|
36
|
+
print(df.to_string(index=False))
|
|
37
|
+
print("-" * 60)
|
|
38
|
+
print(f" Total NMTCs: ${self.total_nmtcs:,.0f}")
|
|
39
|
+
print(f" PV of Credits: ${self.pv_credits:,.0f} (@ {self.discount_rate*100:.1f}% discount rate)")
|
|
40
|
+
print()
|
|
41
|
+
return df
|
|
42
|
+
|
|
43
|
+
def to_dict(self) -> dict:
|
|
44
|
+
return {
|
|
45
|
+
"project_name": self.project_name,
|
|
46
|
+
"qei": self.qei,
|
|
47
|
+
"total_nmtcs": self.total_nmtcs,
|
|
48
|
+
"annual_credits": self.annual_credits,
|
|
49
|
+
"cumulative_credits": self.cumulative_credits,
|
|
50
|
+
"pv_credits": self.pv_credits,
|
|
51
|
+
"discount_rate": self.discount_rate,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def schedule(deal: NMTCDeal) -> CreditScheduleResult:
|
|
56
|
+
"""
|
|
57
|
+
Generate the 7-year NMTC tax credit schedule.
|
|
58
|
+
|
|
59
|
+
Credits are earned at:
|
|
60
|
+
- 5% of QEI in years 1, 2, 3
|
|
61
|
+
- 6% of QEI in years 4, 5, 6, 7
|
|
62
|
+
Total: 39% of QEI
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
deal: NMTCDeal instance
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
CreditScheduleResult with annual and cumulative credit schedule
|
|
69
|
+
"""
|
|
70
|
+
annual_credits = []
|
|
71
|
+
for yr in range(1, deal.compliance_years + 1):
|
|
72
|
+
rate = 0.05 if yr <= 3 else 0.06
|
|
73
|
+
annual_credits.append(deal.qei * rate)
|
|
74
|
+
|
|
75
|
+
cumulative_credits = list(np.cumsum(annual_credits))
|
|
76
|
+
|
|
77
|
+
# PV of credits discounted at deal.discount_rate
|
|
78
|
+
pv_credits = sum(
|
|
79
|
+
credit / ((1 + deal.discount_rate) ** yr)
|
|
80
|
+
for yr, credit in enumerate(annual_credits, 1)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return CreditScheduleResult(
|
|
84
|
+
project_name=deal.project_name,
|
|
85
|
+
qei=deal.qei,
|
|
86
|
+
total_nmtcs=deal.total_nmtcs,
|
|
87
|
+
annual_credits=annual_credits,
|
|
88
|
+
cumulative_credits=cumulative_credits,
|
|
89
|
+
pv_credits=pv_credits,
|
|
90
|
+
discount_rate=deal.discount_rate,
|
|
91
|
+
)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from nmtccalc.data.schema import NMTCDeal
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class InvestorResult:
|
|
10
|
+
"""Output object from investor economics analysis."""
|
|
11
|
+
project_name: str
|
|
12
|
+
investor_equity: float
|
|
13
|
+
annual_credits: list
|
|
14
|
+
total_nmtcs: float
|
|
15
|
+
credit_price: float
|
|
16
|
+
gross_benefit: float
|
|
17
|
+
net_benefit: float
|
|
18
|
+
irr: float
|
|
19
|
+
moic: float
|
|
20
|
+
|
|
21
|
+
def summary(self) -> pd.DataFrame:
|
|
22
|
+
rows = []
|
|
23
|
+
for yr, credit in enumerate(self.annual_credits, 1):
|
|
24
|
+
rows.append({
|
|
25
|
+
"Year": f"Y{yr}",
|
|
26
|
+
"Tax Credit ($)": f"${credit:,.0f}",
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
df = pd.DataFrame(rows)
|
|
30
|
+
print(f"\nInvestor Economics — {self.project_name}")
|
|
31
|
+
print(f"Equity In: ${self.investor_equity/1e6:.2f}MM | Credit Price: ${self.credit_price:.2f}/$1")
|
|
32
|
+
print("-" * 50)
|
|
33
|
+
print(df.to_string(index=False))
|
|
34
|
+
print("-" * 50)
|
|
35
|
+
print(f" Total NMTCs: ${self.total_nmtcs:,.0f}")
|
|
36
|
+
print(f" Gross Benefit: ${self.gross_benefit:,.0f}")
|
|
37
|
+
print(f" Net Benefit: ${self.net_benefit:,.0f}")
|
|
38
|
+
print(f" MOIC: {self.moic:.2f}x")
|
|
39
|
+
print(f" IRR: {self.irr*100:.1f}%")
|
|
40
|
+
print()
|
|
41
|
+
return df
|
|
42
|
+
|
|
43
|
+
def to_dict(self) -> dict:
|
|
44
|
+
return {
|
|
45
|
+
"project_name": self.project_name,
|
|
46
|
+
"investor_equity": self.investor_equity,
|
|
47
|
+
"total_nmtcs": self.total_nmtcs,
|
|
48
|
+
"credit_price": self.credit_price,
|
|
49
|
+
"gross_benefit": self.gross_benefit,
|
|
50
|
+
"net_benefit": self.net_benefit,
|
|
51
|
+
"irr": self.irr,
|
|
52
|
+
"moic": self.moic,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _compute_irr(cash_flows: list) -> float:
|
|
57
|
+
"""Compute IRR using numpy."""
|
|
58
|
+
coeffs = cash_flows[::-1]
|
|
59
|
+
try:
|
|
60
|
+
roots = np.roots(coeffs)
|
|
61
|
+
real_roots = [r.real for r in roots if abs(r.imag) < 1e-6 and r.real > 0]
|
|
62
|
+
if not real_roots:
|
|
63
|
+
return float("nan")
|
|
64
|
+
irr = min(real_roots) - 1
|
|
65
|
+
return irr
|
|
66
|
+
except Exception:
|
|
67
|
+
return float("nan")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def analyze(deal: NMTCDeal) -> InvestorResult:
|
|
71
|
+
"""
|
|
72
|
+
Compute investor economics for an NMTC transaction.
|
|
73
|
+
|
|
74
|
+
The investor puts in equity upfront and receives tax credits
|
|
75
|
+
over the 7-year compliance period.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
deal: NMTCDeal instance
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
InvestorResult with IRR, MOIC, and credit schedule
|
|
82
|
+
"""
|
|
83
|
+
from nmtccalc.models.credits import schedule
|
|
84
|
+
|
|
85
|
+
credit_result = schedule(deal)
|
|
86
|
+
annual_credits = credit_result.annual_credits
|
|
87
|
+
|
|
88
|
+
gross_benefit = deal.total_nmtcs
|
|
89
|
+
net_benefit = gross_benefit - deal.investor_equity
|
|
90
|
+
|
|
91
|
+
# Cash flows: negative equity upfront, credits received each year
|
|
92
|
+
cash_flows = [-deal.investor_equity] + annual_credits
|
|
93
|
+
|
|
94
|
+
# IRR via numpy polynomial roots
|
|
95
|
+
irr = _compute_irr(cash_flows)
|
|
96
|
+
|
|
97
|
+
moic = sum(annual_credits) / deal.investor_equity
|
|
98
|
+
|
|
99
|
+
return InvestorResult(
|
|
100
|
+
project_name=deal.project_name,
|
|
101
|
+
investor_equity=deal.investor_equity,
|
|
102
|
+
annual_credits=annual_credits,
|
|
103
|
+
total_nmtcs=deal.total_nmtcs,
|
|
104
|
+
credit_price=deal.credit_price,
|
|
105
|
+
gross_benefit=gross_benefit,
|
|
106
|
+
net_benefit=net_benefit,
|
|
107
|
+
irr=irr,
|
|
108
|
+
moic=moic,
|
|
109
|
+
)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
import pandas as pd
|
|
3
|
+
|
|
4
|
+
from nmtccalc.data.schema import NMTCDeal
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class SubsidyResult:
|
|
9
|
+
"""Output object from NMTC net subsidy analysis."""
|
|
10
|
+
project_name: str
|
|
11
|
+
total_project_cost: float
|
|
12
|
+
qei: float
|
|
13
|
+
investor_equity: float
|
|
14
|
+
cde_fee: float
|
|
15
|
+
qlici_b_loan: float
|
|
16
|
+
net_subsidy: float
|
|
17
|
+
net_subsidy_pct: float
|
|
18
|
+
effective_cost_of_capital: float
|
|
19
|
+
interest_savings_7yr: float
|
|
20
|
+
|
|
21
|
+
def summary(self) -> pd.DataFrame:
|
|
22
|
+
rows = [
|
|
23
|
+
("Investor Equity (into fund)", f"${self.investor_equity/1e6:.2f}MM"),
|
|
24
|
+
("Less: CDE Fee", f"(${self.cde_fee/1e6:.2f}MM)"),
|
|
25
|
+
("B Loan to QALICB", f"${self.qlici_b_loan/1e6:.2f}MM"),
|
|
26
|
+
("", ""),
|
|
27
|
+
("Net Subsidy (est. forgiven)", f"${self.net_subsidy/1e6:.2f}MM"),
|
|
28
|
+
("Net Subsidy as % of Project", f"{self.net_subsidy_pct*100:.1f}%"),
|
|
29
|
+
("", ""),
|
|
30
|
+
("Effective Cost of Capital", f"{self.effective_cost_of_capital*100:.2f}%"),
|
|
31
|
+
("Interest Savings (7yr)", f"${self.interest_savings_7yr/1e6:.2f}MM"),
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
df = pd.DataFrame(rows, columns=["Item", "Value"])
|
|
35
|
+
print(f"\nNet Subsidy Analysis — {self.project_name}")
|
|
36
|
+
print("=" * 50)
|
|
37
|
+
print(df.to_string(index=False))
|
|
38
|
+
print()
|
|
39
|
+
return df
|
|
40
|
+
|
|
41
|
+
def to_dict(self) -> dict:
|
|
42
|
+
return {
|
|
43
|
+
"project_name": self.project_name,
|
|
44
|
+
"net_subsidy": self.net_subsidy,
|
|
45
|
+
"net_subsidy_pct": self.net_subsidy_pct,
|
|
46
|
+
"effective_cost_of_capital": self.effective_cost_of_capital,
|
|
47
|
+
"interest_savings_7yr": self.interest_savings_7yr,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def analyze(deal: NMTCDeal) -> SubsidyResult:
|
|
52
|
+
"""
|
|
53
|
+
Compute the net subsidy and effective cost of capital for the QALICB.
|
|
54
|
+
|
|
55
|
+
The net subsidy is the B Loan amount that is typically forgiven
|
|
56
|
+
via put/call at the end of the 7-year compliance period.
|
|
57
|
+
This represents the real economic benefit to the project.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
deal: NMTCDeal instance
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
SubsidyResult with net subsidy and effective cost metrics
|
|
64
|
+
"""
|
|
65
|
+
# Net subsidy: B Loan is typically forgiven at end of compliance period
|
|
66
|
+
net_subsidy = deal.qlici_b_loan
|
|
67
|
+
net_subsidy_pct = net_subsidy / deal.total_project_cost
|
|
68
|
+
|
|
69
|
+
# Interest savings: difference between market rate and QLICI B loan rate
|
|
70
|
+
# over the 7-year compliance period on the B loan principal
|
|
71
|
+
market_rate = deal.leverage_loan_rate # use leverage rate as market proxy
|
|
72
|
+
interest_savings_7yr = deal.qlici_b_loan * (
|
|
73
|
+
market_rate - deal.qlici_b_loan_rate
|
|
74
|
+
) * deal.compliance_years
|
|
75
|
+
|
|
76
|
+
# Effective cost of capital: blended rate on total QLICI
|
|
77
|
+
# weighted average of A and B loan rates by their principal amounts
|
|
78
|
+
total_qlici = deal.qlici_total
|
|
79
|
+
if total_qlici > 0:
|
|
80
|
+
effective_cost_of_capital = (
|
|
81
|
+
(deal.qlici_a_loan * deal.qlici_a_loan_rate) +
|
|
82
|
+
(deal.qlici_b_loan * deal.qlici_b_loan_rate)
|
|
83
|
+
) / total_qlici
|
|
84
|
+
else:
|
|
85
|
+
effective_cost_of_capital = 0.0
|
|
86
|
+
|
|
87
|
+
return SubsidyResult(
|
|
88
|
+
project_name=deal.project_name,
|
|
89
|
+
total_project_cost=deal.total_project_cost,
|
|
90
|
+
qei=deal.qei,
|
|
91
|
+
investor_equity=deal.investor_equity,
|
|
92
|
+
cde_fee=deal.cde_fee,
|
|
93
|
+
qlici_b_loan=deal.qlici_b_loan,
|
|
94
|
+
net_subsidy=net_subsidy,
|
|
95
|
+
net_subsidy_pct=net_subsidy_pct,
|
|
96
|
+
effective_cost_of_capital=effective_cost_of_capital,
|
|
97
|
+
interest_savings_7yr=interest_savings_7yr,
|
|
98
|
+
)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
import pandas as pd
|
|
3
|
+
|
|
4
|
+
from nmtccalc.data.schema import NMTCDeal
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class TransactionResult:
|
|
9
|
+
"""Output object from NMTC transaction structure analysis."""
|
|
10
|
+
project_name: str
|
|
11
|
+
total_project_cost: float
|
|
12
|
+
qei: float
|
|
13
|
+
total_nmtcs: float
|
|
14
|
+
investor_equity: float
|
|
15
|
+
leverage_loan: float
|
|
16
|
+
cde_fee: float
|
|
17
|
+
qlici_total: float
|
|
18
|
+
qlici_a_loan: float
|
|
19
|
+
qlici_b_loan: float
|
|
20
|
+
credit_price: float
|
|
21
|
+
nmtc_coverage: float # NMTCs as % of total project cost
|
|
22
|
+
leverage_ratio: float # leverage loan / investor equity
|
|
23
|
+
|
|
24
|
+
def summary(self) -> pd.DataFrame:
|
|
25
|
+
rows = [
|
|
26
|
+
("Total Project Cost", f"${self.total_project_cost/1e6:.2f}MM"),
|
|
27
|
+
("── QEI (NMTC Allocation)", f"${self.qei/1e6:.2f}MM"),
|
|
28
|
+
("── Total NMTCs (39% × QEI)", f"${self.total_nmtcs/1e6:.2f}MM"),
|
|
29
|
+
("", ""),
|
|
30
|
+
("INVESTMENT FUND", ""),
|
|
31
|
+
("── Investor Equity", f"${self.investor_equity/1e6:.2f}MM"),
|
|
32
|
+
("── Leverage Loan", f"${self.leverage_loan/1e6:.2f}MM"),
|
|
33
|
+
("── Total QEI", f"${self.qei/1e6:.2f}MM"),
|
|
34
|
+
("", ""),
|
|
35
|
+
("CDE / SUB-CDE", ""),
|
|
36
|
+
("── CDE Fee", f"${self.cde_fee/1e6:.2f}MM"),
|
|
37
|
+
("── Total QLICI", f"${self.qlici_total/1e6:.2f}MM"),
|
|
38
|
+
("", ""),
|
|
39
|
+
("QLICI TO QALICB", ""),
|
|
40
|
+
("── A Loan (Senior)", f"${self.qlici_a_loan/1e6:.2f}MM"),
|
|
41
|
+
("── B Loan (Subordinate)", f"${self.qlici_b_loan/1e6:.2f}MM"),
|
|
42
|
+
("", ""),
|
|
43
|
+
("KEY RATIOS", ""),
|
|
44
|
+
("── Credit Price", f"${self.credit_price:.2f} per $1 of NMTCs"),
|
|
45
|
+
("── NMTC Coverage", f"{self.nmtc_coverage*100:.1f}% of project cost"),
|
|
46
|
+
("── Leverage Ratio", f"{self.leverage_ratio:.2f}x"),
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
df = pd.DataFrame(rows, columns=["Item", "Amount"])
|
|
50
|
+
print(f"\nNMTC Transaction Structure — {self.project_name}")
|
|
51
|
+
print("=" * 55)
|
|
52
|
+
print(df.to_string(index=False))
|
|
53
|
+
print()
|
|
54
|
+
return df
|
|
55
|
+
|
|
56
|
+
def to_dict(self) -> dict:
|
|
57
|
+
return {
|
|
58
|
+
"project_name": self.project_name,
|
|
59
|
+
"total_project_cost": self.total_project_cost,
|
|
60
|
+
"qei": self.qei,
|
|
61
|
+
"total_nmtcs": self.total_nmtcs,
|
|
62
|
+
"investor_equity": self.investor_equity,
|
|
63
|
+
"leverage_loan": self.leverage_loan,
|
|
64
|
+
"cde_fee": self.cde_fee,
|
|
65
|
+
"qlici_total": self.qlici_total,
|
|
66
|
+
"qlici_a_loan": self.qlici_a_loan,
|
|
67
|
+
"qlici_b_loan": self.qlici_b_loan,
|
|
68
|
+
"credit_price": self.credit_price,
|
|
69
|
+
"nmtc_coverage": self.nmtc_coverage,
|
|
70
|
+
"leverage_ratio": self.leverage_ratio,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def structure(deal: NMTCDeal) -> TransactionResult:
|
|
75
|
+
"""
|
|
76
|
+
Compute the full NMTC leveraged transaction structure.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
deal: NMTCDeal instance with all deal parameters
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
TransactionResult with complete capital stack breakdown
|
|
83
|
+
"""
|
|
84
|
+
nmtc_coverage = deal.total_nmtcs / deal.total_project_cost
|
|
85
|
+
leverage_ratio = deal.leverage_loan / deal.investor_equity
|
|
86
|
+
|
|
87
|
+
return TransactionResult(
|
|
88
|
+
project_name=deal.project_name,
|
|
89
|
+
total_project_cost=deal.total_project_cost,
|
|
90
|
+
qei=deal.qei,
|
|
91
|
+
total_nmtcs=deal.total_nmtcs,
|
|
92
|
+
investor_equity=deal.investor_equity,
|
|
93
|
+
leverage_loan=deal.leverage_loan,
|
|
94
|
+
cde_fee=deal.cde_fee,
|
|
95
|
+
qlici_total=deal.qlici_total,
|
|
96
|
+
qlici_a_loan=deal.qlici_a_loan,
|
|
97
|
+
qlici_b_loan=deal.qlici_b_loan,
|
|
98
|
+
credit_price=deal.credit_price,
|
|
99
|
+
nmtc_coverage=nmtc_coverage,
|
|
100
|
+
leverage_ratio=leverage_ratio,
|
|
101
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=42", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "nmtc-calc"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python calculator for New Markets Tax Credit (NMTC) leveraged transactions"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
dependencies = [
|
|
13
|
+
"pandas>=1.4.0",
|
|
14
|
+
"numpy>=1.21.0",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.urls]
|
|
18
|
+
Homepage = "https://github.com/Jaypatel1511/nmtc-calc"
|
nmtc_calc-0.1.0/setup.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from nmtccalc.models import credits
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_seven_years(sample_deal):
|
|
7
|
+
result = credits.schedule(sample_deal)
|
|
8
|
+
assert len(result.annual_credits) == 7
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_years_1_to_3_rate(sample_deal):
|
|
12
|
+
result = credits.schedule(sample_deal)
|
|
13
|
+
for yr in range(3):
|
|
14
|
+
assert result.annual_credits[yr] == pytest.approx(sample_deal.qei * 0.05)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_years_4_to_7_rate(sample_deal):
|
|
18
|
+
result = credits.schedule(sample_deal)
|
|
19
|
+
for yr in range(3, 7):
|
|
20
|
+
assert result.annual_credits[yr] == pytest.approx(sample_deal.qei * 0.06)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_total_credits_equals_39pct(sample_deal):
|
|
24
|
+
result = credits.schedule(sample_deal)
|
|
25
|
+
assert sum(result.annual_credits) == pytest.approx(sample_deal.qei * 0.39)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_cumulative_final_equals_total(sample_deal):
|
|
29
|
+
result = credits.schedule(sample_deal)
|
|
30
|
+
assert result.cumulative_credits[-1] == pytest.approx(sample_deal.total_nmtcs)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_pv_less_than_total(sample_deal):
|
|
34
|
+
result = credits.schedule(sample_deal)
|
|
35
|
+
assert result.pv_credits < result.total_nmtcs
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_summary_returns_dataframe(sample_deal):
|
|
39
|
+
result = credits.schedule(sample_deal)
|
|
40
|
+
df = result.summary()
|
|
41
|
+
assert isinstance(df, pd.DataFrame)
|
|
42
|
+
assert len(df) == 7
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_to_dict_keys(sample_deal):
|
|
46
|
+
result = credits.schedule(sample_deal)
|
|
47
|
+
d = result.to_dict()
|
|
48
|
+
assert "annual_credits" in d
|
|
49
|
+
assert "pv_credits" in d
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from nmtccalc.models import investor
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_moic_positive(sample_deal):
|
|
7
|
+
result = investor.analyze(sample_deal)
|
|
8
|
+
assert result.moic > 0
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_moic_math(sample_deal):
|
|
12
|
+
result = investor.analyze(sample_deal)
|
|
13
|
+
expected_moic = sample_deal.total_nmtcs / sample_deal.investor_equity
|
|
14
|
+
assert result.moic == pytest.approx(expected_moic)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_gross_benefit_equals_total_nmtcs(sample_deal):
|
|
18
|
+
result = investor.analyze(sample_deal)
|
|
19
|
+
assert result.gross_benefit == pytest.approx(sample_deal.total_nmtcs)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_net_benefit_math(sample_deal):
|
|
23
|
+
result = investor.analyze(sample_deal)
|
|
24
|
+
expected = sample_deal.total_nmtcs - sample_deal.investor_equity
|
|
25
|
+
assert result.net_benefit == pytest.approx(expected)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_irr_is_float(sample_deal):
|
|
29
|
+
result = investor.analyze(sample_deal)
|
|
30
|
+
assert isinstance(result.irr, float)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_summary_returns_dataframe(sample_deal):
|
|
34
|
+
result = investor.analyze(sample_deal)
|
|
35
|
+
df = result.summary()
|
|
36
|
+
assert isinstance(df, pd.DataFrame)
|
|
37
|
+
assert len(df) == 7
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_to_dict_keys(sample_deal):
|
|
41
|
+
result = investor.analyze(sample_deal)
|
|
42
|
+
d = result.to_dict()
|
|
43
|
+
assert "irr" in d
|
|
44
|
+
assert "moic" in d
|
|
45
|
+
assert "net_benefit" in d
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from nmtccalc.models import subsidy
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_net_subsidy_positive(sample_deal):
|
|
7
|
+
result = subsidy.analyze(sample_deal)
|
|
8
|
+
assert result.net_subsidy > 0
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_net_subsidy_equals_b_loan(sample_deal):
|
|
12
|
+
result = subsidy.analyze(sample_deal)
|
|
13
|
+
assert result.net_subsidy == pytest.approx(sample_deal.qlici_b_loan)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_net_subsidy_pct_range(sample_deal):
|
|
17
|
+
result = subsidy.analyze(sample_deal)
|
|
18
|
+
assert 0.10 <= result.net_subsidy_pct <= 0.35
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_effective_cost_below_market(sample_deal):
|
|
22
|
+
result = subsidy.analyze(sample_deal)
|
|
23
|
+
assert result.effective_cost_of_capital < sample_deal.leverage_loan_rate
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_interest_savings_positive(sample_deal):
|
|
27
|
+
result = subsidy.analyze(sample_deal)
|
|
28
|
+
assert result.interest_savings_7yr > 0
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_summary_returns_dataframe(sample_deal):
|
|
32
|
+
result = subsidy.analyze(sample_deal)
|
|
33
|
+
df = result.summary()
|
|
34
|
+
assert isinstance(df, pd.DataFrame)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_to_dict_keys(sample_deal):
|
|
38
|
+
result = subsidy.analyze(sample_deal)
|
|
39
|
+
d = result.to_dict()
|
|
40
|
+
assert "net_subsidy" in d
|
|
41
|
+
assert "net_subsidy_pct" in d
|
|
42
|
+
assert "effective_cost_of_capital" in d
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from nmtccalc.data.schema import NMTCDeal
|
|
3
|
+
from nmtccalc.models import transaction
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_total_nmtcs(sample_deal):
|
|
7
|
+
assert sample_deal.total_nmtcs == pytest.approx(10_000_000 * 0.39)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_investor_equity(sample_deal):
|
|
11
|
+
expected = 10_000_000 * 0.39 * 0.83
|
|
12
|
+
assert sample_deal.investor_equity == pytest.approx(expected)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_leverage_loan(sample_deal):
|
|
16
|
+
expected = 10_000_000 - (10_000_000 * 0.39 * 0.83)
|
|
17
|
+
assert sample_deal.leverage_loan == pytest.approx(expected)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_qei_equals_equity_plus_loan(sample_deal):
|
|
21
|
+
assert sample_deal.qei == pytest.approx(
|
|
22
|
+
sample_deal.investor_equity + sample_deal.leverage_loan
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_qlici_total(sample_deal):
|
|
27
|
+
expected = 10_000_000 - (10_000_000 * 0.02)
|
|
28
|
+
assert sample_deal.qlici_total == pytest.approx(expected)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_qlici_a_plus_b_equals_total(sample_deal):
|
|
32
|
+
assert pytest.approx(sample_deal.qlici_a_loan + sample_deal.qlici_b_loan,
|
|
33
|
+
rel=1e-4) == sample_deal.qlici_total
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_structure_returns_result(sample_deal):
|
|
37
|
+
result = transaction.structure(sample_deal)
|
|
38
|
+
assert result.qei == sample_deal.qei
|
|
39
|
+
assert result.leverage_ratio > 0
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_summary_returns_dataframe(sample_deal):
|
|
43
|
+
import pandas as pd
|
|
44
|
+
result = transaction.structure(sample_deal)
|
|
45
|
+
df = result.summary()
|
|
46
|
+
assert isinstance(df, pd.DataFrame)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_allocation_exceeds_project_cost_raises():
|
|
50
|
+
with pytest.raises(ValueError, match="cannot exceed"):
|
|
51
|
+
NMTCDeal(
|
|
52
|
+
project_name="Bad Deal",
|
|
53
|
+
total_project_cost=5_000_000,
|
|
54
|
+
nmtc_allocation=6_000_000,
|
|
55
|
+
credit_price=0.83,
|
|
56
|
+
leverage_loan_rate=0.045,
|
|
57
|
+
qlici_a_loan_rate=0.045,
|
|
58
|
+
qlici_b_loan_rate=0.01,
|
|
59
|
+
cde_fee_rate=0.02,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_to_dict_keys(sample_deal):
|
|
64
|
+
result = transaction.structure(sample_deal)
|
|
65
|
+
d = result.to_dict()
|
|
66
|
+
assert "qei" in d
|
|
67
|
+
assert "leverage_loan" in d
|
|
68
|
+
assert "nmtc_coverage" in d
|