Suitability and Reg BI

Record a rationale for every recommendation. SEC Best Interest fiduciary paper trail.

SEC Regulation Best Interest (Reg BI) and the broader fiduciary standard require a documented basis for why each recommendation serves the client’s best interest. horizon.compliance.suitability captures that basis at decision time and threads it through the audit log.

The module records suitability; it does not judge it. The rationale comes from the advisor, strategy, or PM desk. The module’s job is to make the reasoning auditable.

Import

python
from horizon.compliance import (
    RiskAlignment,
    SuitabilityAssessment,
    SuitabilityBuilder,
)

Minimum: attach rationale to an order

python
from horizon.types import OrderAction

builder = SuitabilityBuilder(audit_log=audit_log, actor="adv_smith")

action = OrderAction.place(
    market_id="AAPL", side="buy", quantity=100, price=180.0,
    account_id="acc_jane_taxable",
    client_order_id="coid_1",
    strategy_id="momentum_v2",
)

# Returns a new OrderAction with the assessment id in metadata.
action = builder.attach(
    account=jane_account,
    action=action,
    rationale="Mean-reversion entry in Jane's tech sleeve; aligns with growth objective.",
    risk_match=RiskAlignment.Aligned,
    objective_match=("growth", "tech_exposure"),
    alternatives_considered=("MSFT", "GOOG"),
    advisor_id="adv_smith",
)

venue.submit(action)

attach() builds the assessment, records an AuditCategory.SuitabilityRecord event, and returns a new OrderAction with metadata["suitability_assessment_id"]. The id flows with the order through best-ex and statements.

Alternatively, use record() to persist an assessment without attaching it to an action (for example, when recommending a portfolio allocation change rather than a specific order).

What’s captured

python
@dataclass(frozen=True)
class SuitabilityAssessment:
    assessment_id: str
    account_id: str
    market_id: str
    side: str
    quantity: float
    rationale: str
    risk_match: str                    # "aligned" | "above" | "below" | "unknown"
    objective_match: tuple[str, ...]   # ("growth", "income", ...)
    alternatives_considered: tuple[str, ...]
    advisor_id: str                    # who recorded the recommendation
    ips_document_hash: str             # IPS version in force (from Account.ips)
    client_id: str | None
    strategy_id: str
    signal_id: str
    supporting_data: dict              # model outputs, signals, news refs
    recorded_at: datetime

The audit event payload is the full dict. Examiners can reconstruct the reasoning deterministically.

Risk alignment

python
RiskAlignment.Aligned   # matches the client's stated tolerance
RiskAlignment.Below     # more conservative than tolerance
RiskAlignment.Above     # more aggressive than tolerance
RiskAlignment.Unknown   # tolerance not on file

Use Above with a non-empty rationale when an aggressive trade is intentional. A later examiner can filter for Above + rationale to review exception cases.

IPS version fingerprinting

SuitabilityAssessment.ips_document_hash mirrors Account.ips.ips_document_hash. When the client signs a new IPS, the hash changes; every assessment recorded afterwards carries the new hash. The audit log proves which IPS version was in force at the time of any recommendation.

Free-form supporting data

python
builder.attach(
    account=jane_account,
    action=action,
    rationale="BollingerMeanRev long entry; Sharpe 1.4 YTD.",
    supporting_data={
        "model_version": "bmr_v2.3",
        "signal_ids": ["sig_1234", "sig_1235"],
        "market_regime": "low_vol_uptrend",
        "news_articles_considered": 0,
    },
)

The supporting_data dict serializes as part of the audit payload. Keep it structured: examiners grep it.

Reading suitability records

python
from horizon.audit import AuditCategory

events = [
    e for e in audit_log._sink.read_range()
    if e.category == AuditCategory.SuitabilityRecord
    and e.account_id == "acc_jane_taxable"
]
for e in events:
    a = e.payload
    print(a["recorded_at"], a["market_id"], a["rationale"])

Reg BI disclosures are separate

The Reg BI delivery package (Form CRS, Best Interest disclosures, account opening paperwork) is counsel’s work product. This module records the rationale; it does not draft the disclosures. A later tool can render assessments into a client-facing rationale letter.

Out of scope

  • Suitability questionnaires. Initial client onboarding (risk tolerance, investment horizon, net worth) belongs to your CRM or custodian onboarding form, not to the SDK.
  • Conflict-of-interest disclosures. Captured elsewhere; reference the controlling document in supporting_data.
  • Auto-generated rationales. Every recommendation needs a human (or explicit strategy) to supply the rationale. Empty rationales raise ValueError.

Related

  • Accounts and IPS defines the policy envelope each recommendation must fit inside.
  • Restrictions enforces hard blocks; suitability documents the why for recommendations that pass.
  • Audit trail stores every assessment.