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
from horizon.compliance import (
RiskAlignment,
SuitabilityAssessment,
SuitabilityBuilder,
)
Minimum: attach rationale to an order
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
@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
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
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
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.