Import credit-fund loans into NAV (eNAV HLD-LOANS schedule)

The eNAV importer read only the HLD securities sheet, so a credit fund's
loan book (its largest asset) never reached NAV. SATS Credit Fund showed
$3.6M of securities while holding a $46.3M loan to Fold Holdings.

Parse the HLD-LOANS schedule alongside HLD: each loan becomes a holding
and position valued at principal plus accrued interest, flowing into
holdings, invested cost, and the quarter NAV. Adds test coverage and
bumps the package to 0.2.47.
This commit is contained in:
Jonathan Kirkwood
2026-09-02 08:50:31 -05:00
parent ee8e1f30a1
commit 12a6e33bd8
5 changed files with 221 additions and 9 deletions
+84
View File
@@ -0,0 +1,84 @@
"""Credit-fund loan schedule: the eNAV 'HLD-LOANS' sheet must fold into holdings and NAV.
Regression for the SATS Credit Fund case where the Portal showed $3.6M of securities while
the fund held a $46.3M loan — because the loan lives on a separate schedule, not the HLD
Holdings Report. See import_router._parse_loans.
"""
import io
from datetime import date, datetime
import openpyxl
from ten31portal.routers.import_router import _parse_schedule_xlsx
def _enav_with_loan() -> bytes:
"""Minimal eNAV: an HLD securities sheet plus an HLD-LOANS loan schedule."""
wb = openpyxl.Workbook()
hld = wb.active
hld.title = "HLD"
hld["A1"] = datetime(2025, 12, 31)
hld.append([None])
hld.append(["SECURITY NAME", "QUANTITY", "COST BASIS - BOOK", "MARKET VALUE (BOOK)"])
hld.append(["BTC", 18.74, 1_734_584.37, 1_639_942.63])
hld.append(["Fold Holdings, Inc. - Warrant 03/26/25", 3_702_360, 0, 0])
hld.append(["TOTAL - LONG", None, 1_734_584.37, 3_597_442.63])
loans = wb.create_sheet("HLD-LOANS")
loans.append(["SATS CREDIT FUND LP"])
loans.append([None])
# Header + one loan row + a total row the parser must skip.
loans.append([
"COUNTERPART", "SECURITY NAME", "ISSUER NAME", "TRADE DATE",
"PRINCIPAL BALANCE", "TOTAL COST", "VALUE",
"VALUE PLUS ACCRUED INTEREST REC / (PAY)",
])
loans.append([
"Fold Holdings, Inc.", "Fold Holdings, Inc. (Floating Rate)", "Fold Holdings, Inc.",
datetime(2025, 3, 26), 46_279_500, 46_279_500, 46_279_500, 46_494_185.46,
])
loans.append(["TOTAL - LOAN", None, None, None, 46_279_500, 46_279_500, 46_279_500, 46_494_185.46])
out = io.BytesIO()
wb.save(out)
return out.getvalue()
def test_loan_schedule_folds_into_holdings_and_nav():
name, as_of, holdings, positions, errors = _parse_schedule_xlsx(_enav_with_loan())
assert errors == []
assert as_of == date(2025, 12, 31)
# The loan groups under its issuer, which the warrant already created: no duplicate holding.
assert [h["company_name"] for h in holdings].count("Fold Holdings, Inc.") == 1
loan = next(p for p in positions if p["security_name"] == "Fold Holdings, Inc. (Floating Rate)")
assert loan["company_name"] == "Fold Holdings, Inc."
assert loan["value_cents"] == 46_494_185_46 # principal + accrued interest, in cents
assert loan["cost_cents"] == 46_279_500_00
assert loan["investment_date"] == "2025-03-26"
assert loan["shares"] is None
# NAV is the sum of every position value: securities ($3.6M) + the loan ($46.5M).
nav = sum(p["value_cents"] or 0 for p in positions)
assert nav == 1_639_942_63 + 0 + 46_494_185_46
def test_no_loan_sheet_is_a_noop():
"""A workbook with no HLD-LOANS sheet parses the securities exactly as before."""
wb = openpyxl.Workbook()
hld = wb.active
hld.title = "HLD"
hld["A1"] = datetime(2025, 12, 31)
hld.append([None])
hld.append(["SECURITY NAME", "QUANTITY", "COST BASIS - BOOK", "MARKET VALUE (BOOK)"])
hld.append(["Acme - Series A", 10, 100.0, 900.0])
out = io.BytesIO()
wb.save(out)
_, _, holdings, positions, errors = _parse_schedule_xlsx(out.getvalue())
assert errors == []
assert len(positions) == 1
assert positions[0]["value_cents"] == 900_00