Compliance & suitability monitoring

Compliance and suitability monitoring on daily fund data

Suitability is not a one-off check at onboarding. Risk ratings drift, funds change objective, holdings concentrate and documents go stale. This page describes a monitoring loop that re-reads each fund's published facts daily, compares them with the last known state and raises alerts an operations team can act on.

The problem

A client is placed in a fund with risk rating 4 that fits their profile. Eighteen months later the fund house republishes the KID with a rating of 5. Nothing in the portfolio system changes; the position looks the same. Unless someone re-reads the document, the mismatch goes unnoticed until an audit or a complaint.

The same applies to concentration, where a fund's top holding grows from 6 percent to 12 percent, and to document freshness, where the most recent factsheet available is months old. Compliance teams know these are the checks to run; the difficulty is running them across every fund, every day, without a data team.

What you need from the data

A monitoring loop compares a small set of attributes against their previous values:

  • Risk rating (1–7) and the profile risk band
  • Top holding weight and the number of holdings, as concentration proxies
  • Region focus and sector tilt, to detect mandate drift
  • Investment objective text, to detect wording changes
  • Asset allocation, for multi-asset funds that should stay within bands
  • dataAsOf, to detect stale documents
  • Whether the fund still exists (a 404 on a held fund)

For each, you need both the current value and a stored previous value, plus a rule that says what change is material.

How it works with FundFacts API

Run a daily job over held ISINs. For each, load the fund, compute a snapshot of the monitored attributes, diff it against yesterday's snapshot and emit alerts.

typescript
type Snapshot = {
riskRating?: number;
riskBand?: string;
topWeight?: number;
regionFocus?: string;
objective?: string;
dataAsOf?: string;
};
export async function snapshot(isin: string): Promise<Snapshot | "gone"> {
const res = await fetch(`https://fundfactsapi.com/api/v1/funds/${isin}`, {
headers: { Authorization: `Bearer ${process.env.FUNDFACTS_API_KEY}` },
});
if (res.status === 404) return "gone";
if (!res.ok) throw new Error(`${res.status} loading ${isin}`);
const { data } = await res.json();
return {
riskRating: data.riskRating,
riskBand: data.profile?.riskBand,
topWeight: data.topHoldings?.[0]?.weight,
regionFocus: data.profile?.regionFocus,
objective: data.investmentObjective,
dataAsOf: data.dataAsOf,
};
}
export function alerts(prev: Snapshot, next: Snapshot, staleDays = 120): string[] {
const out: string[] = [];
if (prev.riskRating !== next.riskRating)
out.push(`risk rating ${prev.riskRating} -> ${next.riskRating}`);
if ((next.topWeight ?? 0) > 10 && (prev.topWeight ?? 0) <= 10)
out.push(`top holding above 10%: ${next.topWeight}%`);
if (prev.regionFocus !== next.regionFocus)
out.push(`region focus ${prev.regionFocus} -> ${next.regionFocus}`);
if (prev.objective !== next.objective) out.push("investment objective text changed");
if (next.dataAsOf) {
const age = (Date.now() - Date.parse(next.dataAsOf)) / 86_400_000;
if (age > staleDays) out.push(`documents ${Math.round(age)} days old`);
}
return out;
}

Route alerts by severity: a risk-rating change on a fund held by clients at the edge of their band is urgent; a stale document is a ticket. The SRRI explainer covers how the rating is derived and why it moves.

Design notes

Caching and cadence. The API refreshes every 24 hours, so a daily job is the right cadence; running more often gains nothing. Store every snapshot with its expiresAt and dataAsOf. The history is your audit trail and lets you answer "when did we first see this rating".

Quota. One request per held ISIN per day. A firm holding a few hundred distinct funds fits a Pro or Scale plan; watch X-RateLimit-Remaining and finish the run over two days if needed rather than dropping funds. On 429, pause until X-RateLimit-Reset.

Errors. Treat 502 upstream_error as "no snapshot today", not as a change. Do not raise a false alert because the fund house's site was unavailable. A 404 fund_not_found on a fund that resolved yesterday is itself an alert.

Thresholds are yours. The API supplies facts; what counts as material concentration or an acceptable document age is a policy decision that belongs in your compliance manual, not in code defaults.

Text diffs. Objective wording changes for trivial reasons (a typo fix, a reformatted PDF). Normalise whitespace and case before comparing, and show the diff to a reviewer rather than auto-escalating.

Disclaimers. Alerts describe changes in published facts and are not a judgement on the fund. Display dataAsOf on every alert, and keep the suitability decision with the adviser and the firm's process. Related reading: MiFID II and KID.

Frequently asked questions

How quickly will a risk-rating change appear in the API?

Each fund's published documents are re-read on a 24-hour cycle, so a new KID is reflected within a day of the fund house publishing it. Your monitoring job then sees the new riskRating on its next run. The lag that matters is usually the fund house's own publication schedule, which is why storing dataAsOf with each snapshot is important for the audit trail.

Can the API tell me directly when something changed?

No. The endpoint returns the current state of a fund; detecting change is done by comparing today's response with the snapshot you stored yesterday. This is deliberate: your firm decides which fields matter and what threshold is material. The pattern is a few dozen lines of code and gives you a complete history of every monitored attribute.

Does the API return the SRRI or the SRI?

riskRating is the 1–7 indicator from the fund's current key information document. For UCITS KIIDs that is the SRRI; for PRIIPs KIDs it is the SRI. The two methodologies differ, so a fund that moved from one document format to the other may show a step change that is not a change in the fund itself. Note the document type when reviewing such alerts.

What should happen when a held fund returns 404?

Treat it as an alert rather than a silent skip. A 404 fund_not_found on a fund that resolved the day before usually means the share class has been closed, merged or renamed, all of which are events a compliance team needs to review. Keep the last good snapshot for the record and assign the position to a human for follow-up.

Build your compliance & suitability monitoring on FundFacts API

Free plan with 10 requests a day to prototype; paid plans add the AI companion kit that scaffolds the integration for you.