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
from horizon.fund import (
FundVehicle, FundConfig,
UnitLedger, UnitHolder,
Subscription, Redemption,
NavBuilder, NavSample, compute_nav,
)
from horizon.accounts import Account, AccountSubType
Minimal fund lifecycle
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
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:
unitsgrows byunits_issued.total_subscribed_usdgrows byamount_usd.cost_basis_usdgrows byamount_usd.first_subscribed_atis set on the first subscription; used for lockup.
Redemption
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
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:
- Portfolio equity. Cash plus marked-to-market positions. Comes from the ledger (
result.equity_curve, or the live loop’s per-tick mark). - Accrued but unpaid fees. Reduce NAV until they crystallize. Plug in
horizon.accounting.fees.accrue_period_feesand subtract. - Units outstanding. From
UnitLedger.units_outstanding(at=ts).
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
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:
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
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:
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.
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
ManagementFeeConfiglevel by adjustingannual_bpsper period. - In-kind subscriptions / redemptions. All events are cash-denominated today.