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

python
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

text
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.

python
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.

python
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.

python
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.

python
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 with FundVehicle for 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.

python
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.

python
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.

Status by phase

L0 - Model classes: Household, Client, Account, Custodian, IPS - AccountRegistry with CRUD and queries - `Account.allows(asset_class)` gate - `ips_document_hash` field for linking to the signed PDF - `advisory_fee_bps_per_year`, `performance_fee_pct`, `high_water_mark` fields
L1 - `hz.run()` accepts `accounts=registry` and emits orders per account - Risk engine enforces IPS at order time - Tax-lot accounting per account. See [Tax lots](/docs/professionals/tax-lots). - Best-ex logging keyed by `account_id`. See [Best execution](/docs/professionals/best-execution). - PDT tracking per margin account. See [Risk layers](/docs/professionals/risk-layers).
L2 - Fund sub-type: daily NAV, units, subscriptions, redemptions - Client statements per account per period. See [Statements](/docs/professionals/statements). - Advisory fee assessment (AUM bps + carry). See [Fees](/docs/professionals/fees). - Tax packets. See [Tax packets](/docs/professionals/tax-packets). - Form ADV / AUM rollup
Not in SDK - Client PII storage (belongs at the custodian). - Form ADV filing (counsel's work product). - Fiduciary judgment (advisor's responsibility).