Accounts and IPS
Household, Client, Account, Custodian. Investment Policy Statement per account. SMA, Fund, or firm prop.
Out of the box Horizon is single-portfolio: one ledger, one cash balance, one set of positions. That fits research and prop trading. It does not fit an advisor running 80 client accounts across three custodians, each with its own IPS.
horizon.accounts is the model. Nothing in Tiers 1 to 3 reads from it. When opted in, the execution loop keys every order, fill, and risk decision by account_id. See Running for how hz.run(accounts=registry, ...) activates this.
Import
from horizon.accounts import (
Household, Client, Account, Custodian,
InvestmentPolicyStatement, AccountRegistry,
AccountSubType, AccountTaxType, RiskTolerance, TaxBracket,
)
from horizon.asset_classes import AssetClass
from horizon.types import TaxLotElection
The model
Custodian (Schwab, Fidelity, Apex, IBKR Prime, ...)
│
└── Account (one per client per tax-type per custodian)
│
├── client_id ──► Client ──► Household
│
└── ips InvestmentPolicyStatement
│
├── allowed_asset_classes
├── max_pct_single_issuer
├── allow_margin / allow_shorts / allow_options / allow_crypto
├── max_drawdown_pct, target_volatility_pct
├── excluded_tickers / sectors / ESG screens
└── ips_document_hash (SHA-256 of the signed PDF)
Custodians
A Custodian is a firm that holds client assets. venue_name links into the execution layer: it must match a Venue.venue_name of the driver that places orders at that custodian. See Venues.
schwab = Custodian(
custodian_id="cust_schwab",
display_name="Schwab Advisor Services",
venue_name="schwab", # matches a Venue impl
supports=(AssetClass.Equity, AssetClass.Option),
qualified_custodian=True, # SEC 206(4)-2(d)(6)
advisor_has_custody=False,
)
ibkr = Custodian(
custodian_id="cust_ibkr",
display_name="Interactive Brokers Prime",
venue_name="ibkr",
supports=(AssetClass.Equity, AssetClass.Option, AssetClass.Future),
)
Clients and households
PII (legal name, DOB, SSN, address) lives in the custodian’s system or an encrypted KYC store. It is not in Client. The SDK carries only enough for attribution.
household = Household(
household_id="hh_smith",
display_name="Smith Family",
client_ids=("cli_jane", "cli_bob"),
primary_client_id="cli_jane",
)
jane = Client(
client_id="cli_jane",
display_name="Jane Smith", # non-PII handle only
household_id="hh_smith",
risk_tolerance=RiskTolerance.ModeratelyAggressive,
tax_bracket=TaxBracket.Mid,
tax_residency="US",
state_residency="NY",
is_accredited=True, # 3(c)(1) / Reg D 506(c) gate
is_qualified_purchaser=False, # 3(c)(7) gate
)
Investment Policy Statement
InvestmentPolicyStatement is the machine-enforceable subset of the full IPS. The full document (PDF, signed by the client) lives in the record-keeping store. Record its SHA-256 hash in ips_document_hash so the audit log can prove which version was in force.
jane_ips = InvestmentPolicyStatement(
allowed_asset_classes=frozenset([AssetClass.Equity, AssetClass.Option]),
max_pct_per_asset_class={
AssetClass.Equity: 0.95,
AssetClass.Option: 0.20,
},
max_pct_single_issuer=0.10,
max_pct_single_sector=0.30,
min_cash_buffer_pct=0.05,
allow_margin=False,
allow_shorts=False,
allow_options=True,
options_level=2, # broker options-approval 0-4
allow_crypto=False,
allow_prediction=False,
max_drawdown_pct=0.20,
excluded_tickers=frozenset({"TSLA"}),
esg_screens=frozenset({"no_tobacco", "no_fossil_fuels"}),
rebalance_tolerance_pct=0.05,
rebalance_cadence_days=30,
ips_document_id="ips_smith_v3",
ips_document_hash="7c4a...e9", # SHA-256 of the signed PDF
effective_date=date(2026, 1, 15),
)
Every field is optional with a safe default. Start with asset-class allowlist and max drawdown; expand as the program matures.
Accounts
An Account is the unit the execution loop iterates over. Each client typically has several: taxable brokerage, Traditional IRA, Roth IRA, joint account, trust account, each with its own tax treatment and sometimes its own IPS.
jane_taxable = Account(
account_id="acc_jane_taxable",
sub_type=AccountSubType.SMA,
display_name="Jane Taxable Brokerage",
client_id="cli_jane",
household_id="hh_smith",
custodian_id="cust_schwab",
custodian_account_number="XXXX-9321",
venue_name="schwab",
ips=jane_ips,
asset_class_allowlist=frozenset([AssetClass.Equity, AssetClass.Option]),
tax_type=AccountTaxType.Taxable,
tax_lot_election=TaxLotElection.HIFO, # tax-loss harvesting default
initial_funding_usd=500_000,
funded_at=datetime(2026, 2, 1, tzinfo=timezone.utc),
advisory_fee_bps_per_year=100, # 1% AUM fee
)
jane_ira = Account(
account_id="acc_jane_ira",
sub_type=AccountSubType.SMA,
display_name="Jane Traditional IRA",
client_id="cli_jane",
household_id="hh_smith",
custodian_id="cust_schwab",
venue_name="schwab",
tax_type=AccountTaxType.TraditionalIRA,
tax_lot_election=TaxLotElection.FIFO, # no harvesting in IRAs
ips=jane_ips,
)
Account sub-types
AccountSubType.SMA. Separately Managed Account. One beneficial owner. Most advisor accounts.AccountSubType.Fund. Pooled vehicle (LP / LLC). Wrap withFundVehiclefor daily NAV, unit accounting, subscriptions and redemptions.AccountSubType.Firm. The advisor’s own trading. Typically segregated from client money.
Registering everything
AccountRegistry is the in-memory collection the execution loop consults each tick. L1 backs it with the persistent ledger.
registry = AccountRegistry()
registry.add_custodian(schwab)
registry.add_custodian(ibkr)
registry.add_household(household)
registry.add_client(jane)
registry.add_client(bob)
registry.add_account(jane_taxable)
registry.add_account(jane_ira)
registry.accounts() # all active accounts
registry.accounts_for_client("cli_jane") # [jane_taxable, jane_ira]
registry.accounts_for_household("hh_smith")
registry.accounts_for_custodian("cust_schwab")
# Bulk load:
registry.load(
custodians=[schwab, ibkr],
households=[household],
clients=[jane, bob],
accounts=[jane_taxable, jane_ira, bob_joint],
)
Pre-trade policy checks
Account.allows(asset_class) is the fast gate. Both the account’s allowlist and the IPS must permit the asset class.
jane_taxable.allows(AssetClass.Equity) # True
jane_taxable.allows(AssetClass.Crypto) # False
The richer checks (single-issuer caps, sector caps, per-account drawdown, excluded-ticker, options-level) run in the L1 risk engine. See Risk layers.