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.
@@ -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,2 @@
1
+ pandas>=1.4.0
2
+ numpy>=1.21.0
@@ -0,0 +1 @@
1
+ nmtccalc
@@ -0,0 +1,5 @@
1
+ from nmtccalc.data.schema import NMTCDeal
2
+ from nmtccalc.models import transaction, credits, investor, subsidy
3
+
4
+ __version__ = "0.1.0"
5
+ __all__ = ["NMTCDeal", "transaction", "credits", "investor", "subsidy"]
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"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,11 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="nmtc-calc",
5
+ version="0.1.0",
6
+ packages=find_packages(),
7
+ install_requires=[
8
+ "pandas>=1.4.0",
9
+ "numpy>=1.21.0",
10
+ ],
11
+ )
@@ -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