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.
85 lines
3.2 KiB
Python
85 lines
3.2 KiB
Python
"""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
|