fdic-mcp-server 1.25.2 → 1.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +114 -13
- package/dist/server.js +114 -13
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -32,7 +32,7 @@ var import_types = require("@modelcontextprotocol/sdk/types.js");
|
|
|
32
32
|
var import_express2 = __toESM(require("express"));
|
|
33
33
|
|
|
34
34
|
// src/constants.ts
|
|
35
|
-
var VERSION = true ? "1.
|
|
35
|
+
var VERSION = true ? "1.26.0" : process.env.npm_package_version ?? "0.0.0-dev";
|
|
36
36
|
var FDIC_API_BASE_URL = "https://banks.data.fdic.gov/api";
|
|
37
37
|
var CHARACTER_LIMIT = 5e4;
|
|
38
38
|
var DEFAULT_FDIC_MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
|
|
@@ -32043,6 +32043,62 @@ var FdicBankDeepDiveOutputSchema = import_zod2.z.object({
|
|
|
32043
32043
|
warnings: import_zod2.z.array(import_zod2.z.string()),
|
|
32044
32044
|
sources: import_zod2.z.array(Source)
|
|
32045
32045
|
});
|
|
32046
|
+
var PeerStatsSchema = import_zod2.z.object({
|
|
32047
|
+
peer_count: import_zod2.z.number().int(),
|
|
32048
|
+
peer_median: import_zod2.z.number(),
|
|
32049
|
+
peer_mean: import_zod2.z.number(),
|
|
32050
|
+
subject_value: import_zod2.z.number(),
|
|
32051
|
+
subject_percentile: import_zod2.z.number(),
|
|
32052
|
+
robust_z_score: import_zod2.z.number(),
|
|
32053
|
+
is_outlier: import_zod2.z.boolean(),
|
|
32054
|
+
outlier_direction: import_zod2.z.enum(["high", "low"]).optional()
|
|
32055
|
+
});
|
|
32056
|
+
var PeerHealthMetricRowSchema = import_zod2.z.object({
|
|
32057
|
+
name: import_zod2.z.string(),
|
|
32058
|
+
label: import_zod2.z.string(),
|
|
32059
|
+
subject: import_zod2.z.number().nullable(),
|
|
32060
|
+
peer_median: import_zod2.z.number().nullable(),
|
|
32061
|
+
peer_weighted_avg: import_zod2.z.number().nullable(),
|
|
32062
|
+
percentile: import_zod2.z.number().nullable(),
|
|
32063
|
+
higher_is_better: import_zod2.z.boolean(),
|
|
32064
|
+
is_outlier: import_zod2.z.boolean(),
|
|
32065
|
+
outlier_direction: import_zod2.z.enum(["high", "low"]).nullable()
|
|
32066
|
+
});
|
|
32067
|
+
var PeerHealthInstitutionSchema = import_zod2.z.object({
|
|
32068
|
+
cert: import_zod2.z.number().int(),
|
|
32069
|
+
name: import_zod2.z.string(),
|
|
32070
|
+
name_source: import_zod2.z.enum(["fdic_institution_profile", "cert_fallback"]),
|
|
32071
|
+
city: import_zod2.z.string().nullable(),
|
|
32072
|
+
state: import_zod2.z.string().nullable(),
|
|
32073
|
+
total_assets: import_zod2.z.number().nullable(),
|
|
32074
|
+
proxy_score: import_zod2.z.number(),
|
|
32075
|
+
proxy_band: import_zod2.z.string(),
|
|
32076
|
+
composite_rating: import_zod2.z.number(),
|
|
32077
|
+
composite_label: import_zod2.z.string(),
|
|
32078
|
+
component_ratings: import_zod2.z.record(import_zod2.z.number()),
|
|
32079
|
+
flags: import_zod2.z.array(import_zod2.z.string())
|
|
32080
|
+
});
|
|
32081
|
+
var FdicPeerHealthOutputSchema = import_zod2.z.object({
|
|
32082
|
+
model: import_zod2.z.literal("public_camels_proxy_v1"),
|
|
32083
|
+
official_status: import_zod2.z.literal("public off-site proxy, not official CAMELS"),
|
|
32084
|
+
proxy: import_zod2.z.unknown().nullable(),
|
|
32085
|
+
report_date: import_zod2.z.string(),
|
|
32086
|
+
sort_by: import_zod2.z.string(),
|
|
32087
|
+
total_institutions: import_zod2.z.number().int(),
|
|
32088
|
+
returned_count: import_zod2.z.number().int(),
|
|
32089
|
+
subject_cert: import_zod2.z.number().int().nullable(),
|
|
32090
|
+
subject_rank: import_zod2.z.number().int().nullable(),
|
|
32091
|
+
metrics: import_zod2.z.array(PeerHealthMetricRowSchema),
|
|
32092
|
+
institutions: import_zod2.z.array(PeerHealthInstitutionSchema),
|
|
32093
|
+
peer_context: import_zod2.z.object({
|
|
32094
|
+
peer_count: import_zod2.z.number().int(),
|
|
32095
|
+
peer_definition: import_zod2.z.string(),
|
|
32096
|
+
broadening_steps: import_zod2.z.array(import_zod2.z.string()),
|
|
32097
|
+
subject_rank: import_zod2.z.number().int().nullable(),
|
|
32098
|
+
subject_percentiles: import_zod2.z.record(PeerStatsSchema),
|
|
32099
|
+
weighted_peer_averages: import_zod2.z.record(import_zod2.z.number())
|
|
32100
|
+
}).nullable()
|
|
32101
|
+
}).passthrough();
|
|
32046
32102
|
var FdicAnalysisOutputSchema = import_zod2.z.object({}).passthrough();
|
|
32047
32103
|
|
|
32048
32104
|
// src/tools/institutions.ts
|
|
@@ -35819,6 +35875,15 @@ function computePeerStats(subjectValue, peerValues, options) {
|
|
|
35819
35875
|
}
|
|
35820
35876
|
|
|
35821
35877
|
// src/tools/peerHealth.ts
|
|
35878
|
+
var PEER_METRICS = [
|
|
35879
|
+
// legacyKey preserves the original camelCase peer_context map keys for backward compatibility.
|
|
35880
|
+
// New UI consumers should bind to the flat metrics[].name snake_case values instead.
|
|
35881
|
+
{ key: "roaPct", legacyKey: "roaPct", name: "roa_pct", label: "Return on assets", higherIsBetter: true },
|
|
35882
|
+
{ key: "equityCapitalRatioPct", legacyKey: "equityCapitalRatioPct", name: "equity_capital_ratio_pct", label: "Equity capital ratio", higherIsBetter: true },
|
|
35883
|
+
{ key: "netInterestMarginPct", legacyKey: "netInterestMarginPct", name: "net_interest_margin_pct", label: "Net interest margin", higherIsBetter: true },
|
|
35884
|
+
{ key: "efficiencyRatioPct", legacyKey: "efficiencyRatioPct", name: "efficiency_ratio_pct", label: "Efficiency ratio", higherIsBetter: false },
|
|
35885
|
+
{ key: "loanToDepositPct", legacyKey: "loanToDepositPct", name: "loan_to_deposit_pct", label: "Loan-to-deposit ratio", higherIsBetter: false }
|
|
35886
|
+
];
|
|
35822
35887
|
var PeerHealthInputSchema = import_zod11.z.object({
|
|
35823
35888
|
cert: import_zod11.z.number().int().positive().optional().describe("Subject institution CERT to highlight in the ranking. Optional."),
|
|
35824
35889
|
certs: import_zod11.z.array(import_zod11.z.number().int().positive()).max(50).optional().describe("Explicit list of CERTs to compare (max 50)."),
|
|
@@ -35853,11 +35918,11 @@ Three usage modes:
|
|
|
35853
35918
|
|
|
35854
35919
|
Optionally provide cert to highlight a subject institution's position in the ranking.
|
|
35855
35920
|
|
|
35856
|
-
Output:
|
|
35921
|
+
Output: structuredContent includes {model, official_status, report_date, institutions, metrics, peer_context, proxy}. Institutions include proxy scores and name_source. When a subject cert is provided, metrics is a flat subject-vs-peer array for UI binding while peer_context preserves the legacy nested percentiles and weighted averages. Auto-peer selection derives asset bands from report-date financials and broadens the cohort if fewer than 10 peers match.
|
|
35857
35922
|
|
|
35858
35923
|
NOTE: Public off-site analytical proxy \u2014 not official supervisory ratings.`,
|
|
35859
35924
|
inputSchema: PeerHealthInputSchema,
|
|
35860
|
-
outputSchema:
|
|
35925
|
+
outputSchema: FdicPeerHealthOutputSchema,
|
|
35861
35926
|
annotations: {
|
|
35862
35927
|
readOnlyHint: true,
|
|
35863
35928
|
destructiveHint: false,
|
|
@@ -36031,6 +36096,33 @@ NOTE: Public off-site analytical proxy \u2014 not official supervisory ratings.`
|
|
|
36031
36096
|
const c = asNumber(r.CERT);
|
|
36032
36097
|
if (c !== null) profileMap.set(c, r);
|
|
36033
36098
|
}
|
|
36099
|
+
const financialCerts = allFinancials.map((r) => asNumber(r.CERT)).filter((cert) => cert !== null);
|
|
36100
|
+
const missingProfileCerts = financialCerts.filter((cert) => {
|
|
36101
|
+
const profile = profileMap.get(cert);
|
|
36102
|
+
return !profile || typeof profile.NAME !== "string" || profile.NAME.length === 0;
|
|
36103
|
+
});
|
|
36104
|
+
if (missingProfileCerts.length > 0) {
|
|
36105
|
+
const missingProfileFilters = buildCertFilters(missingProfileCerts);
|
|
36106
|
+
const missingProfileResponses = await mapWithConcurrency(
|
|
36107
|
+
missingProfileFilters,
|
|
36108
|
+
MAX_CONCURRENCY,
|
|
36109
|
+
async (certFilter) => queryEndpoint(
|
|
36110
|
+
ENDPOINTS.INSTITUTIONS,
|
|
36111
|
+
{
|
|
36112
|
+
filters: certFilter,
|
|
36113
|
+
fields: "CERT,NAME,CITY,STALP",
|
|
36114
|
+
limit: 1e4,
|
|
36115
|
+
sort_by: "CERT",
|
|
36116
|
+
sort_order: "ASC"
|
|
36117
|
+
},
|
|
36118
|
+
{ signal: controller.signal }
|
|
36119
|
+
)
|
|
36120
|
+
);
|
|
36121
|
+
for (const r of missingProfileResponses.flatMap(extractRecords)) {
|
|
36122
|
+
const c = asNumber(r.CERT);
|
|
36123
|
+
if (c !== null) profileMap.set(c, r);
|
|
36124
|
+
}
|
|
36125
|
+
}
|
|
36034
36126
|
await sendProgressNotification(server.server, progressToken, 0.7, "Computing proxy assessments");
|
|
36035
36127
|
const subjectHistory = params.cert ? await fetchHistoryEvents(params.cert, { signal: controller.signal, repdte: params.repdte }) : [];
|
|
36036
36128
|
let subjectProxy = null;
|
|
@@ -36069,7 +36161,8 @@ NOTE: Public off-site analytical proxy \u2014 not official supervisory ratings.`
|
|
|
36069
36161
|
];
|
|
36070
36162
|
entries.push({
|
|
36071
36163
|
cert,
|
|
36072
|
-
name:
|
|
36164
|
+
name: typeof profile?.NAME === "string" && profile.NAME.length > 0 ? profile.NAME : `CERT ${cert}`,
|
|
36165
|
+
name_source: typeof profile?.NAME === "string" && profile.NAME.length > 0 ? "fdic_institution_profile" : "cert_fallback",
|
|
36073
36166
|
city: profile?.CITY ? String(profile.CITY) : null,
|
|
36074
36167
|
state: profile?.STALP ? String(profile.STALP) : null,
|
|
36075
36168
|
total_assets: asNumber(fin.ASSET),
|
|
@@ -36094,18 +36187,12 @@ NOTE: Public off-site analytical proxy \u2014 not official supervisory ratings.`
|
|
|
36094
36187
|
});
|
|
36095
36188
|
const subjectRank = params.cert ? entries.findIndex((e) => e.cert === params.cert) + 1 : null;
|
|
36096
36189
|
let peerContext = null;
|
|
36190
|
+
let metricRows = [];
|
|
36097
36191
|
if (params.cert) {
|
|
36098
36192
|
const subjectFin = allFinancials.find((f) => asNumber(f.CERT) === params.cert);
|
|
36099
36193
|
if (subjectFin) {
|
|
36100
36194
|
const subjectExtraction = extractCanonicalMetrics(subjectFin);
|
|
36101
36195
|
const sm = subjectExtraction.metrics;
|
|
36102
|
-
const PEER_METRICS = [
|
|
36103
|
-
{ key: "roaPct", label: "roaPct", higherIsBetter: true },
|
|
36104
|
-
{ key: "equityCapitalRatioPct", label: "equityCapitalRatioPct", higherIsBetter: true },
|
|
36105
|
-
{ key: "netInterestMarginPct", label: "netInterestMarginPct", higherIsBetter: true },
|
|
36106
|
-
{ key: "efficiencyRatioPct", label: "efficiencyRatioPct", higherIsBetter: false },
|
|
36107
|
-
{ key: "loanToDepositPct", label: "loanToDepositPct", higherIsBetter: false }
|
|
36108
|
-
];
|
|
36109
36196
|
const peerFinancials = allFinancials.filter((f) => asNumber(f.CERT) !== params.cert);
|
|
36110
36197
|
const peerExtractions = peerFinancials.map((f) => extractCanonicalMetrics(f));
|
|
36111
36198
|
const subjectPercentiles = {};
|
|
@@ -36124,15 +36211,28 @@ NOTE: Public off-site analytical proxy \u2014 not official supervisory ratings.`
|
|
|
36124
36211
|
}
|
|
36125
36212
|
}
|
|
36126
36213
|
if (peerValues.length === 0) continue;
|
|
36214
|
+
let stats = null;
|
|
36127
36215
|
if (subjectVal !== null) {
|
|
36128
|
-
|
|
36216
|
+
stats = computePeerStats(subjectVal, peerValues, {
|
|
36129
36217
|
higherIsBetter: pm.higherIsBetter
|
|
36130
36218
|
});
|
|
36219
|
+
subjectPercentiles[pm.legacyKey] = stats;
|
|
36131
36220
|
}
|
|
36132
36221
|
const weighted = computeWeightedAggregate(weightedEntries);
|
|
36133
36222
|
if (weighted !== null) {
|
|
36134
|
-
weightedPeerAverages[pm.
|
|
36223
|
+
weightedPeerAverages[pm.legacyKey] = Math.round(weighted * 100) / 100;
|
|
36135
36224
|
}
|
|
36225
|
+
metricRows.push({
|
|
36226
|
+
name: pm.name,
|
|
36227
|
+
label: pm.label,
|
|
36228
|
+
subject: subjectVal,
|
|
36229
|
+
peer_median: stats?.peer_median ?? null,
|
|
36230
|
+
peer_weighted_avg: weighted !== null ? Math.round(weighted * 100) / 100 : null,
|
|
36231
|
+
percentile: stats?.subject_percentile ?? null,
|
|
36232
|
+
higher_is_better: pm.higherIsBetter,
|
|
36233
|
+
is_outlier: stats?.is_outlier ?? false,
|
|
36234
|
+
outlier_direction: stats?.outlier_direction ?? null
|
|
36235
|
+
});
|
|
36136
36236
|
}
|
|
36137
36237
|
let peerDef;
|
|
36138
36238
|
if (params.certs) {
|
|
@@ -36213,6 +36313,7 @@ NOTE: Public off-site analytical proxy \u2014 not official supervisory ratings.`
|
|
|
36213
36313
|
returned_count: returned.length,
|
|
36214
36314
|
subject_cert: params.cert ?? null,
|
|
36215
36315
|
subject_rank: subjectRank,
|
|
36316
|
+
metrics: metricRows,
|
|
36216
36317
|
institutions: returned,
|
|
36217
36318
|
peer_context: peerContext
|
|
36218
36319
|
}
|
package/dist/server.js
CHANGED
|
@@ -47,7 +47,7 @@ var import_types = require("@modelcontextprotocol/sdk/types.js");
|
|
|
47
47
|
var import_express2 = __toESM(require("express"));
|
|
48
48
|
|
|
49
49
|
// src/constants.ts
|
|
50
|
-
var VERSION = true ? "1.
|
|
50
|
+
var VERSION = true ? "1.26.0" : process.env.npm_package_version ?? "0.0.0-dev";
|
|
51
51
|
var FDIC_API_BASE_URL = "https://banks.data.fdic.gov/api";
|
|
52
52
|
var CHARACTER_LIMIT = 5e4;
|
|
53
53
|
var DEFAULT_FDIC_MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
|
|
@@ -32058,6 +32058,62 @@ var FdicBankDeepDiveOutputSchema = import_zod2.z.object({
|
|
|
32058
32058
|
warnings: import_zod2.z.array(import_zod2.z.string()),
|
|
32059
32059
|
sources: import_zod2.z.array(Source)
|
|
32060
32060
|
});
|
|
32061
|
+
var PeerStatsSchema = import_zod2.z.object({
|
|
32062
|
+
peer_count: import_zod2.z.number().int(),
|
|
32063
|
+
peer_median: import_zod2.z.number(),
|
|
32064
|
+
peer_mean: import_zod2.z.number(),
|
|
32065
|
+
subject_value: import_zod2.z.number(),
|
|
32066
|
+
subject_percentile: import_zod2.z.number(),
|
|
32067
|
+
robust_z_score: import_zod2.z.number(),
|
|
32068
|
+
is_outlier: import_zod2.z.boolean(),
|
|
32069
|
+
outlier_direction: import_zod2.z.enum(["high", "low"]).optional()
|
|
32070
|
+
});
|
|
32071
|
+
var PeerHealthMetricRowSchema = import_zod2.z.object({
|
|
32072
|
+
name: import_zod2.z.string(),
|
|
32073
|
+
label: import_zod2.z.string(),
|
|
32074
|
+
subject: import_zod2.z.number().nullable(),
|
|
32075
|
+
peer_median: import_zod2.z.number().nullable(),
|
|
32076
|
+
peer_weighted_avg: import_zod2.z.number().nullable(),
|
|
32077
|
+
percentile: import_zod2.z.number().nullable(),
|
|
32078
|
+
higher_is_better: import_zod2.z.boolean(),
|
|
32079
|
+
is_outlier: import_zod2.z.boolean(),
|
|
32080
|
+
outlier_direction: import_zod2.z.enum(["high", "low"]).nullable()
|
|
32081
|
+
});
|
|
32082
|
+
var PeerHealthInstitutionSchema = import_zod2.z.object({
|
|
32083
|
+
cert: import_zod2.z.number().int(),
|
|
32084
|
+
name: import_zod2.z.string(),
|
|
32085
|
+
name_source: import_zod2.z.enum(["fdic_institution_profile", "cert_fallback"]),
|
|
32086
|
+
city: import_zod2.z.string().nullable(),
|
|
32087
|
+
state: import_zod2.z.string().nullable(),
|
|
32088
|
+
total_assets: import_zod2.z.number().nullable(),
|
|
32089
|
+
proxy_score: import_zod2.z.number(),
|
|
32090
|
+
proxy_band: import_zod2.z.string(),
|
|
32091
|
+
composite_rating: import_zod2.z.number(),
|
|
32092
|
+
composite_label: import_zod2.z.string(),
|
|
32093
|
+
component_ratings: import_zod2.z.record(import_zod2.z.number()),
|
|
32094
|
+
flags: import_zod2.z.array(import_zod2.z.string())
|
|
32095
|
+
});
|
|
32096
|
+
var FdicPeerHealthOutputSchema = import_zod2.z.object({
|
|
32097
|
+
model: import_zod2.z.literal("public_camels_proxy_v1"),
|
|
32098
|
+
official_status: import_zod2.z.literal("public off-site proxy, not official CAMELS"),
|
|
32099
|
+
proxy: import_zod2.z.unknown().nullable(),
|
|
32100
|
+
report_date: import_zod2.z.string(),
|
|
32101
|
+
sort_by: import_zod2.z.string(),
|
|
32102
|
+
total_institutions: import_zod2.z.number().int(),
|
|
32103
|
+
returned_count: import_zod2.z.number().int(),
|
|
32104
|
+
subject_cert: import_zod2.z.number().int().nullable(),
|
|
32105
|
+
subject_rank: import_zod2.z.number().int().nullable(),
|
|
32106
|
+
metrics: import_zod2.z.array(PeerHealthMetricRowSchema),
|
|
32107
|
+
institutions: import_zod2.z.array(PeerHealthInstitutionSchema),
|
|
32108
|
+
peer_context: import_zod2.z.object({
|
|
32109
|
+
peer_count: import_zod2.z.number().int(),
|
|
32110
|
+
peer_definition: import_zod2.z.string(),
|
|
32111
|
+
broadening_steps: import_zod2.z.array(import_zod2.z.string()),
|
|
32112
|
+
subject_rank: import_zod2.z.number().int().nullable(),
|
|
32113
|
+
subject_percentiles: import_zod2.z.record(PeerStatsSchema),
|
|
32114
|
+
weighted_peer_averages: import_zod2.z.record(import_zod2.z.number())
|
|
32115
|
+
}).nullable()
|
|
32116
|
+
}).passthrough();
|
|
32061
32117
|
var FdicAnalysisOutputSchema = import_zod2.z.object({}).passthrough();
|
|
32062
32118
|
|
|
32063
32119
|
// src/tools/institutions.ts
|
|
@@ -35834,6 +35890,15 @@ function computePeerStats(subjectValue, peerValues, options) {
|
|
|
35834
35890
|
}
|
|
35835
35891
|
|
|
35836
35892
|
// src/tools/peerHealth.ts
|
|
35893
|
+
var PEER_METRICS = [
|
|
35894
|
+
// legacyKey preserves the original camelCase peer_context map keys for backward compatibility.
|
|
35895
|
+
// New UI consumers should bind to the flat metrics[].name snake_case values instead.
|
|
35896
|
+
{ key: "roaPct", legacyKey: "roaPct", name: "roa_pct", label: "Return on assets", higherIsBetter: true },
|
|
35897
|
+
{ key: "equityCapitalRatioPct", legacyKey: "equityCapitalRatioPct", name: "equity_capital_ratio_pct", label: "Equity capital ratio", higherIsBetter: true },
|
|
35898
|
+
{ key: "netInterestMarginPct", legacyKey: "netInterestMarginPct", name: "net_interest_margin_pct", label: "Net interest margin", higherIsBetter: true },
|
|
35899
|
+
{ key: "efficiencyRatioPct", legacyKey: "efficiencyRatioPct", name: "efficiency_ratio_pct", label: "Efficiency ratio", higherIsBetter: false },
|
|
35900
|
+
{ key: "loanToDepositPct", legacyKey: "loanToDepositPct", name: "loan_to_deposit_pct", label: "Loan-to-deposit ratio", higherIsBetter: false }
|
|
35901
|
+
];
|
|
35837
35902
|
var PeerHealthInputSchema = import_zod11.z.object({
|
|
35838
35903
|
cert: import_zod11.z.number().int().positive().optional().describe("Subject institution CERT to highlight in the ranking. Optional."),
|
|
35839
35904
|
certs: import_zod11.z.array(import_zod11.z.number().int().positive()).max(50).optional().describe("Explicit list of CERTs to compare (max 50)."),
|
|
@@ -35868,11 +35933,11 @@ Three usage modes:
|
|
|
35868
35933
|
|
|
35869
35934
|
Optionally provide cert to highlight a subject institution's position in the ranking.
|
|
35870
35935
|
|
|
35871
|
-
Output:
|
|
35936
|
+
Output: structuredContent includes {model, official_status, report_date, institutions, metrics, peer_context, proxy}. Institutions include proxy scores and name_source. When a subject cert is provided, metrics is a flat subject-vs-peer array for UI binding while peer_context preserves the legacy nested percentiles and weighted averages. Auto-peer selection derives asset bands from report-date financials and broadens the cohort if fewer than 10 peers match.
|
|
35872
35937
|
|
|
35873
35938
|
NOTE: Public off-site analytical proxy \u2014 not official supervisory ratings.`,
|
|
35874
35939
|
inputSchema: PeerHealthInputSchema,
|
|
35875
|
-
outputSchema:
|
|
35940
|
+
outputSchema: FdicPeerHealthOutputSchema,
|
|
35876
35941
|
annotations: {
|
|
35877
35942
|
readOnlyHint: true,
|
|
35878
35943
|
destructiveHint: false,
|
|
@@ -36046,6 +36111,33 @@ NOTE: Public off-site analytical proxy \u2014 not official supervisory ratings.`
|
|
|
36046
36111
|
const c = asNumber(r.CERT);
|
|
36047
36112
|
if (c !== null) profileMap.set(c, r);
|
|
36048
36113
|
}
|
|
36114
|
+
const financialCerts = allFinancials.map((r) => asNumber(r.CERT)).filter((cert) => cert !== null);
|
|
36115
|
+
const missingProfileCerts = financialCerts.filter((cert) => {
|
|
36116
|
+
const profile = profileMap.get(cert);
|
|
36117
|
+
return !profile || typeof profile.NAME !== "string" || profile.NAME.length === 0;
|
|
36118
|
+
});
|
|
36119
|
+
if (missingProfileCerts.length > 0) {
|
|
36120
|
+
const missingProfileFilters = buildCertFilters(missingProfileCerts);
|
|
36121
|
+
const missingProfileResponses = await mapWithConcurrency(
|
|
36122
|
+
missingProfileFilters,
|
|
36123
|
+
MAX_CONCURRENCY,
|
|
36124
|
+
async (certFilter) => queryEndpoint(
|
|
36125
|
+
ENDPOINTS.INSTITUTIONS,
|
|
36126
|
+
{
|
|
36127
|
+
filters: certFilter,
|
|
36128
|
+
fields: "CERT,NAME,CITY,STALP",
|
|
36129
|
+
limit: 1e4,
|
|
36130
|
+
sort_by: "CERT",
|
|
36131
|
+
sort_order: "ASC"
|
|
36132
|
+
},
|
|
36133
|
+
{ signal: controller.signal }
|
|
36134
|
+
)
|
|
36135
|
+
);
|
|
36136
|
+
for (const r of missingProfileResponses.flatMap(extractRecords)) {
|
|
36137
|
+
const c = asNumber(r.CERT);
|
|
36138
|
+
if (c !== null) profileMap.set(c, r);
|
|
36139
|
+
}
|
|
36140
|
+
}
|
|
36049
36141
|
await sendProgressNotification(server.server, progressToken, 0.7, "Computing proxy assessments");
|
|
36050
36142
|
const subjectHistory = params.cert ? await fetchHistoryEvents(params.cert, { signal: controller.signal, repdte: params.repdte }) : [];
|
|
36051
36143
|
let subjectProxy = null;
|
|
@@ -36084,7 +36176,8 @@ NOTE: Public off-site analytical proxy \u2014 not official supervisory ratings.`
|
|
|
36084
36176
|
];
|
|
36085
36177
|
entries.push({
|
|
36086
36178
|
cert,
|
|
36087
|
-
name:
|
|
36179
|
+
name: typeof profile?.NAME === "string" && profile.NAME.length > 0 ? profile.NAME : `CERT ${cert}`,
|
|
36180
|
+
name_source: typeof profile?.NAME === "string" && profile.NAME.length > 0 ? "fdic_institution_profile" : "cert_fallback",
|
|
36088
36181
|
city: profile?.CITY ? String(profile.CITY) : null,
|
|
36089
36182
|
state: profile?.STALP ? String(profile.STALP) : null,
|
|
36090
36183
|
total_assets: asNumber(fin.ASSET),
|
|
@@ -36109,18 +36202,12 @@ NOTE: Public off-site analytical proxy \u2014 not official supervisory ratings.`
|
|
|
36109
36202
|
});
|
|
36110
36203
|
const subjectRank = params.cert ? entries.findIndex((e) => e.cert === params.cert) + 1 : null;
|
|
36111
36204
|
let peerContext = null;
|
|
36205
|
+
let metricRows = [];
|
|
36112
36206
|
if (params.cert) {
|
|
36113
36207
|
const subjectFin = allFinancials.find((f) => asNumber(f.CERT) === params.cert);
|
|
36114
36208
|
if (subjectFin) {
|
|
36115
36209
|
const subjectExtraction = extractCanonicalMetrics(subjectFin);
|
|
36116
36210
|
const sm = subjectExtraction.metrics;
|
|
36117
|
-
const PEER_METRICS = [
|
|
36118
|
-
{ key: "roaPct", label: "roaPct", higherIsBetter: true },
|
|
36119
|
-
{ key: "equityCapitalRatioPct", label: "equityCapitalRatioPct", higherIsBetter: true },
|
|
36120
|
-
{ key: "netInterestMarginPct", label: "netInterestMarginPct", higherIsBetter: true },
|
|
36121
|
-
{ key: "efficiencyRatioPct", label: "efficiencyRatioPct", higherIsBetter: false },
|
|
36122
|
-
{ key: "loanToDepositPct", label: "loanToDepositPct", higherIsBetter: false }
|
|
36123
|
-
];
|
|
36124
36211
|
const peerFinancials = allFinancials.filter((f) => asNumber(f.CERT) !== params.cert);
|
|
36125
36212
|
const peerExtractions = peerFinancials.map((f) => extractCanonicalMetrics(f));
|
|
36126
36213
|
const subjectPercentiles = {};
|
|
@@ -36139,15 +36226,28 @@ NOTE: Public off-site analytical proxy \u2014 not official supervisory ratings.`
|
|
|
36139
36226
|
}
|
|
36140
36227
|
}
|
|
36141
36228
|
if (peerValues.length === 0) continue;
|
|
36229
|
+
let stats = null;
|
|
36142
36230
|
if (subjectVal !== null) {
|
|
36143
|
-
|
|
36231
|
+
stats = computePeerStats(subjectVal, peerValues, {
|
|
36144
36232
|
higherIsBetter: pm.higherIsBetter
|
|
36145
36233
|
});
|
|
36234
|
+
subjectPercentiles[pm.legacyKey] = stats;
|
|
36146
36235
|
}
|
|
36147
36236
|
const weighted = computeWeightedAggregate(weightedEntries);
|
|
36148
36237
|
if (weighted !== null) {
|
|
36149
|
-
weightedPeerAverages[pm.
|
|
36238
|
+
weightedPeerAverages[pm.legacyKey] = Math.round(weighted * 100) / 100;
|
|
36150
36239
|
}
|
|
36240
|
+
metricRows.push({
|
|
36241
|
+
name: pm.name,
|
|
36242
|
+
label: pm.label,
|
|
36243
|
+
subject: subjectVal,
|
|
36244
|
+
peer_median: stats?.peer_median ?? null,
|
|
36245
|
+
peer_weighted_avg: weighted !== null ? Math.round(weighted * 100) / 100 : null,
|
|
36246
|
+
percentile: stats?.subject_percentile ?? null,
|
|
36247
|
+
higher_is_better: pm.higherIsBetter,
|
|
36248
|
+
is_outlier: stats?.is_outlier ?? false,
|
|
36249
|
+
outlier_direction: stats?.outlier_direction ?? null
|
|
36250
|
+
});
|
|
36151
36251
|
}
|
|
36152
36252
|
let peerDef;
|
|
36153
36253
|
if (params.certs) {
|
|
@@ -36228,6 +36328,7 @@ NOTE: Public off-site analytical proxy \u2014 not official supervisory ratings.`
|
|
|
36228
36328
|
returned_count: returned.length,
|
|
36229
36329
|
subject_cert: params.cert ?? null,
|
|
36230
36330
|
subject_rank: subjectRank,
|
|
36331
|
+
metrics: metricRows,
|
|
36231
36332
|
institutions: returned,
|
|
36232
36333
|
peer_context: peerContext
|
|
36233
36334
|
}
|