Fund vehicle

Units, subscriptions, redemptions, daily NAV. 3(c)(1) and 3(c)(7) pooled vehicles built on the Account model.

A fund in Horizon is an Account with sub_type=AccountSubType.Fund plus a FundVehicle that tracks units outstanding, per-holder unit balances, and a NAV time series. Subscriptions mint units at the striking NAV. Redemptions burn units at the striking NAV net of any fee or gate.

Management and performance fees accrue at the fund level (not per holder) and reduce NAV before units are struck. Per-holder economics come out of the unit math naturally.

Import

python
from horizon.fund import (
    FundVehicle, FundConfig,
    UnitLedger, UnitHolder,
    Subscription, Redemption,
    NavBuilder, NavSample, compute_nav,
)
from horizon.accounts import Account, AccountSubType

Minimal fund lifecycle

python
from datetime import date, datetime, timezone

account = Account(
    account_id="fund_1",
    sub_type=AccountSubType.Fund,
    display_name="Pegasus Opportunities Fund I",
    client_id="gp_entity_1",             # the GP / manager entity
    custodian_id="cust_ibkr",
    venue_name="ibkr",
    tax_type=AccountTaxType.Other,
    advisory_fee_bps_per_year=200,       # 2%
    performance_fee_pct=0.20,            # 20% carry
    performance_fee_hurdle_pct=0.06,     # 6% hurdle
)

vehicle = FundVehicle(
    account=account,
    inception_date=date(2026, 1, 1),
    config=FundConfig(
        initial_unit_price=100.0,
        lockup_days=90,
        redemption_fee_bps=50.0,         # 0.5% on early redemptions
    ),
)

# First capital in at seed NAV (100 per unit).
t0 = datetime(2026, 1, 1, tzinfo=timezone.utc)
vehicle.subscribe(holder_id="cli_jane", amount_usd=500_000, at=t0)
vehicle.subscribe(holder_id="cli_bob",  amount_usd=250_000, at=t0)

# End-of-day NAV mark from the backtest/live loop's equity curve.
vehicle.mark_nav(equity_usd=765_000, at=t0 + timedelta(days=1))

# Redemption (after lockup).
vehicle.redeem(holder_id="cli_jane", units=1_000, at=t0 + timedelta(days=100))

FundVehicle enforces sub_type=Fund at construction; passing an SMA account raises ValueError.

The UnitLedger

Source of truth for units outstanding and per-holder balances. Subscriptions and redemptions are the only events that mint or burn units.

Subscription

python
event = vehicle.subscribe(
    holder_id="cli_jane",
    amount_usd=100_000,
    at=datetime.now(timezone.utc),
    # nav_per_unit defaults to vehicle.current_nav_per_unit()
)
event.units_issued           # = amount_usd / nav_per_unit
event.nav_per_unit           # striking NAV

Each subscription also updates the holder:

  • units grows by units_issued.
  • total_subscribed_usd grows by amount_usd.
  • cost_basis_usd grows by amount_usd.
  • first_subscribed_at is set on the first subscription; used for lockup.

Redemption

python
event = vehicle.redeem(
    holder_id="cli_jane",
    units=1_000,
    at=datetime.now(timezone.utc),
)
event.gross_proceeds_usd     # units * nav_per_unit
event.redemption_fee_usd     # from FundConfig.redemption_fee_bps by default
event.net_proceeds_usd       # gross - fee

Cost basis reduces pro-rata: redeeming 20% of your units reduces your cost_basis_usd by 20%. Realized P&L falls out of the algebra: total_redeemed_usd - (total_subscribed_usd - cost_basis_usd).

Per-holder queries

python
h = vehicle.holder("cli_jane")
h.units                              # current unit balance
h.cost_basis_usd                     # attributed to currently-held units
h.current_value_usd(nav_per_unit)    # units * nav_per_unit
h.unrealized_pnl_usd(nav_per_unit)
h.realized_pnl_usd()                 # lifetime realized gain
h.average_cost_per_unit              # cost_basis / units

NAV

Three moving parts:

  1. Portfolio equity. Cash plus marked-to-market positions. Comes from the ledger (result.equity_curve, or the live loop’s per-tick mark).
  2. Accrued but unpaid fees. Reduce NAV until they crystallize. Plug in horizon.accounting.fees.accrue_period_fees and subtract.
  3. Units outstanding. From UnitLedger.units_outstanding(at=ts).
python
NAV per unit = (equity_usd - accrued_fees_usd - other_liabilities_usd)
             / units_outstanding

When units_outstanding == 0, NAV returns the seed price (FundConfig.initial_unit_price, default 100.0) so the first subscription strikes deterministically.

Time series

python
series = vehicle.nav_series(
    equity_curve,                        # [(ts, equity_usd), ...]
    accrued_fees_fn=lambda ts: 0.0,      # optional
    liabilities_fn=lambda ts: 0.0,       # optional
)
for sample in series:
    print(sample.effective_at, sample.nav_per_unit, sample.units_outstanding)

mark_nav(equity_usd=..., at=...) is the persistent version; it appends to vehicle.nav_history and updates current_nav_per_unit().

Fee integration

Fund-level fees accrue via the existing Fees module. The fund wrapper is a convenience:

python
assessments = vehicle.accrue_fees(
    equity_samples=equity_curve,
    period_start=q1_start,
    period_end=q1_end,
    high_water_mark=hwm,   # persistent across quarters
)
for a in assessments:
    print(a.kind, a.amount, a.basis_value)

Because fees reduce NAV before units are struck, per-holder attribution happens automatically. A holder who subscribed when NAV was 100 and redeems when NAV is 110 (post-fee) realizes 10% gain net of the fund’s share of fees, pro-rata to their unit holding.

Lockup and redemption gates

python
config = FundConfig(
    lockup_days=180,                    # no redemptions for the first 180 days
    notice_days=30,                     # advisory for now; not enforced
    redemption_fee_bps=100.0,           # 1% on redemptions
    gate_pct=0.10,                      # max 10% of units redeem per period
)

Lockup is enforced by redeem(). Use enforce_lockup=False for administrative redemptions (e.g. key-person events, regulatory override).

notice_days and gate_pct are advisory fields in this release. Wire your subscription workflow to request_redemption -> approve -> redeem if you need hard enforcement; or extend FundVehicle with a request queue.

Integration with statements and performance

The statement builder accepts any equity curve and cashflow list; pass the fund’s perspective for the fund-level report, and the per-holder cashflow list for a limited-partner report:

python
from horizon.reporting import StatementBuilder, Cashflow

# Fund-level statement
fund_stmt = StatementBuilder().build(
    account=vehicle.account,
    ledger=ledger,
    equity_samples=equity_curve,
    cashflows=[
        Cashflow(s.effective_at, +s.amount_usd, "subscription")
        for s in vehicle.ledger.subscriptions()
    ] + [
        Cashflow(r.effective_at, -r.net_proceeds_usd, "redemption")
        for r in vehicle.ledger.redemptions()
    ],
    period_start=q1_start,
    period_end=q1_end,
)

# Per-LP statement: one Subscription + one Redemption per holder becomes
# their personal cashflow stream; equity curve is scaled by unit share.
for holder in vehicle.holders():
    holder_cashflows = [
        Cashflow(s.effective_at, +s.amount_usd, "subscription")
        for s in vehicle.ledger.subscriptions()
        if s.holder_id == holder.holder_id
    ] + [
        Cashflow(r.effective_at, -r.net_proceeds_usd, "redemption")
        for r in vehicle.ledger.redemptions()
        if r.holder_id == holder.holder_id
    ]
    # Holder equity curve = fund NAV-per-unit * their unit history.
    ...

Performance reports (TWR/MWR/drawdown/Sharpe) work out of the box on the fund equity curve. For per-holder MWR, build the holder’s cashflow series and feed PerformanceBuilder the same way.

Regulatory notes

  • 3(c)(1) and 3(c)(7) exemptions. Client accreditation / qualified-purchaser status lives on Client.is_accredited / Client.is_qualified_purchaser. The fund does not enforce these at the vehicle level; your subscription workflow must. See Accounts and IPS.
  • Performance-fee eligibility. SEC Rule 205-3 requires qualified clients. The fee math does not enforce; the firm is responsible.
  • Custody. The Advisers Act custody rule (206(4)-2) for pooled vehicles has specific audit and surprise-examination requirements. The audit log records every subscription, redemption, and NAV mark for exam replay, but the arrangement itself is counsel’s work.

Multi-class funds

MultiClassFund composes several FundClass views on top of a shared portfolio ledger. Each class has its own fee tier, hurdle, lockup, redemption fee, and minimum subscription. NAV allocation across classes is pro-rata by deposited basis.

python
from horizon.fund.classes import FundClass, MultiClassFund

fund = MultiClassFund(base_fund=vehicle)

fund.add_class(FundClass(
    class_id="A",
    management_fee_bps_per_year=150,   # retail tier
    performance_fee_pct=0.20,
    hurdle=0.06,
    initial_unit_price=100.0,
    lockup_days=90,
    redemption_fee_bps=100,
    minimum_subscription_usd=100_000,
))
fund.add_class(FundClass(
    class_id="I",
    management_fee_bps_per_year=75,    # institutional tier
    performance_fee_pct=0.15,
    hurdle=0.06,
    initial_unit_price=100.0,
    minimum_subscription_usd=5_000_000,
))
fund.add_class(FundClass(
    class_id="Founders",
    management_fee_bps_per_year=0,
    performance_fee_pct=0.10,
    hurdle=0.0,
    initial_unit_price=100.0,
))

fund.subscribe(class_id="I", holder_id="h_1", amount_usd=10_000_000,
               effective_at=t, price_per_unit=100.0)
fund.mark_nav(nav_total_usd=vehicle.ledger.total_equity(), at=t)

Each class maintains its own deposited_basis so the NAV split is robust across class entries and exits at different times.

Out of scope

  • Capital-call schedules. Applies to closed-end / PE funds. Current design assumes open-end with subscriptions at NAV.
  • Catch-up in perf fees. The fees module supports hurdle; catch-up (common in PE fund structures) is an L3 follow-up.
  • Management fee offsets and expense caps. Model at the ManagementFeeConfig level by adjusting annual_bps per period.
  • In-kind subscriptions / redemptions. All events are cash-denominated today.

Related