Form ADV data surfaces
AUM, account counts, client classification, and fee revenue rolled up for Form ADV filings.
Form ADV Part 1 asks a registered investment adviser to report regulatory AUM, account counts by client type, fee revenue, and the advisory services offered. horizon.reporting.adv produces the data surfaces those fields consume.
The module does not file the form. Counsel owns the filing. The SDK makes the numbers auditable and reproducible at any point in time.
Import
from horizon.reporting import AdvBuilder, AdvSnapshot
Quickstart
from datetime import datetime, timezone
from horizon.reporting import AdvBuilder
snap = AdvBuilder().build(
registry=registry, # AccountRegistry
account_values={ # or a callable(aid) -> usd
"acc_jane_taxable": 500_000.0,
"acc_jane_ira": 250_000.0,
"acc_bob_taxable": 100_000.0,
"acc_fund_1": 2_500_000.0,
},
fee_assessments=q1_fees, # list[FeeAssessment]
period_start=datetime(2026, 1, 1, tzinfo=timezone.utc),
period_end=datetime(2026, 3, 31, tzinfo=timezone.utc),
)
print(snap.to_text())
Output:
----------------------------------------------------------------------
Form ADV data surfaces (as of 2026-04-19T00:00:00+00:00)
Period: 2026-01-01 to 2026-03-31
----------------------------------------------------------------------
Regulatory AUM: $ 3,350,000.00
Accounts: 4
Households: 1
Clients: 3
Client classification:
Accredited: 2
Qualified purchaser: 1
Retail (non-accredited): 1
AUM by sub-type:
fund $ 2,500,000.00
sma $ 850,000.00
AUM by asset class:
crypto $ 2,500,000.00
equity $ 725,000.00
option $ 125,000.00
AUM by custodian:
cust_ibkr $ 2,500,000.00
cust_schwab $ 850,000.00
Fee revenue (period):
Total: $ 63,750.00
management $ 13,750.00
performance $ 50,000.00
Accounts w/ perf fee: 1
----------------------------------------------------------------------
snap.to_dict() returns a JSON-safe dict for archival alongside the audit log.
What’s in the snapshot
@dataclass(frozen=True)
class AdvSnapshot:
as_of: datetime
period_start: datetime | None
period_end: datetime | None
total_aum_usd: float
n_accounts: int
n_households: int
n_clients: int
n_accredited: int
n_qualified_purchaser: int
n_retail_non_accredited: int
aum_by_sub_type: dict[str, float]
aum_by_asset_class: dict[str, float]
aum_by_custodian: dict[str, float]
aum_by_tax_type: dict[str, float]
fee_revenue_total_usd: float
fee_revenue_by_kind: dict[str, float]
n_accounts_paying_performance_fee: int
advisory_services: tuple[str, ...]
Client classification
Counts run over the registry’s clients (not accounts):
n_accredited:Client.is_accredited == True.n_qualified_purchaser:Client.is_qualified_purchaser == True.n_retail_non_accredited:not Client.is_accredited.
A client flagged as a qualified purchaser is typically also accredited; the counters are independent so both populate.
AUM by asset class
Each account’s AUM splits evenly across the asset classes in its asset_class_allowlist. This gives a conservative Form ADV estimate when an account is approved for multiple asset classes. Callers with true per-asset valuations can pass a custom account_value_fn and post-process.
Advisory services
Derived from the set of asset classes in use across the registry:
| Asset class in use | Surfaces |
|---|---|
| Equity, Option, Future, Forex | portfolio_management_equity |
| Option | options_strategies |
| Crypto, Perp | digital_asset_advisory |
| Prediction | event_contract_advisory |
AccountSubType.Fund in registry | pooled_vehicle_management |
These flags feed Form ADV 2A Item 4 service descriptions. The module does not write prose; it flags categories that match the firm’s actual activity.
Fee revenue
fee_assessments is a flat list of FeeAssessment records from Fees. The builder sums by kind (management, performance, other) and counts distinct accounts charged a performance fee.
Pass the fee stream from the period you’re reporting on. For the annual Form ADV amendment, pass the full year’s assessments.
Valuation
Two ways to supply account values:
# Static dict
snap = builder.build(registry=r, account_values={"acc_1": 100_000})
# Callable
def value_fn(account_id: str) -> float:
return ledger.equity_by_account(account_id)
snap = builder.build(registry=r, account_values=value_fn)
Accounts without a value default to 0. The reporting window (as_of) does not change the valuation; pass a snapshot already valued at that point.
Archival
For every Form ADV update cycle:
import json
from pathlib import Path
snap = AdvBuilder().build(...)
Path(f"adv/{snap.as_of.date().isoformat()}.json").write_text(
json.dumps(snap.to_dict(), indent=2, default=str)
)
Keep the JSON alongside the audit log for the full retention period. A later examiner can reproduce the numbers by replaying the log and re-running the builder.
Out of scope
- Form ADV filing. Counsel files through IARD. The SDK feeds the numbers.
- Form ADV Part 2 brochure prose. Counsel’s writing. This module provides structured data only.
- Regulatory AUM vs. gross client assets. Form ADV has a specific definition of Regulatory AUM (continuous and regular supervisory services). The module sums all accounts in the registry; exclude any non-discretionary or held-away accounts from the input values if the RAUM treatment requires it.
- GIPS composites. Performance reporting, composite construction, and GIPS verification are out of scope. See Performance for the per-account / per-fund metrics.
Related
- Accounts and IPS defines the client and account model.
- Fund vehicle for pooled-vehicle AUM.
- Fees for the fee-assessment stream.