Aller au contenu

Risk Engine — matrice multi-dimensionnelle + RBA paramétrable

Module : risk-svc (cœur du scoring RBA) · consommé par kyc-svc, aml-svc, case-mgmt-svc. Voir ADR-025, ADR-004, ADR-002.

Le Risk Engine est le cœur de la valeur d’une solution KYC/AML. Il classe chaque client, chaque entité, chaque session d’onboarding en LOW / STANDARD / HIGH / PROHIBITED selon une matrice multi-dimensionnelle paramétrable par tenant, et produit une explication auditable de chaque décision.

Ce document spécifie comment le construire, le brancher et le maintenir.


1. Obligation réglementaire — Risk-Based Approach

Section intitulée « 1. Obligation réglementaire — Risk-Based Approach »
SourceExigence
FATF Reco. 10 + 12 + 15Classification BC/FT obligatoire par client, produit, pays, transaction
6AMLD (UE) art. 18CDD simplifiée / classique / renforcée selon le risque
BCT Circulaire 2017-08 art. 8 + annexe D”Approche par les risques” obligatoire, classification type
Loi 2015-26 TN art. 99-106Revue périodique du profil selon niveau
FinCEN CDD Rule (31 CFR 1010.230)Ongoing monitoring based on risk
BaFin KWG §25hKlassifizierung des Geschäftsrisikos
RGPD art. 22Droit à l’explication des décisions automatisées

→ En audit, le régulateur demande : politique écrite + procédures + preuve qu’elle a été appliquée à chaque client. Le Risk Engine produit les trois.


DimensionVariablesSource
clientPEP (own/family/close_assoc), profession, résidence, âge, marital status, activité proForm Designer + screening
geoPays résidence, pays nationalité, FATF greylist/blacklist, CPI Transparency, sanctions pays (OFAC/UN/EU/BCT)Listes officielles + connecteurs
productType produit, montant moyen attendu, crypto, offshore, non-face-to-face productParamétrage produit banque
channelFace-à-face vs remote, VideoKYC vs selfie, QES vs OTP, agent vs self-serviceWorkflow runtime
aml_screeningPEP hit, sanctions hit, adverse media scoreModule AML screening

Le même engine est réutilisé avec une 6ème dimension transactional (vélocité, cash intensity, pays contreparties) pour scorer en continu les comptes actifs, pas seulement à l’onboarding. Voir AML Transaction Monitoring.

NiveauScoreCDDPiècesDécisionRevueSLA STR
LOW0–25Simplified (SDD)CIN + selfieAuto-approve (si fraud guard > 95%)5 ansN/A
STANDARD26–50Standard CDDCIN + selfie + justif. domicileAgent 1 décide3 ansJ+10
HIGH51–80Enhanced (EDD)+ODF + VideoKYC + 4-eyesAgent 2 + Superviseur1 anJ+5
PROHIBITED81–100RefusCompliance officer + STR6 mois si réévaluéJ+2

riskPolicy("TN-BANQUEX", version = "2.1") {
metadata {
owner = "compliance-officer@banquex.tn"
description = "Politique LCB-FT 2026 — conforme BCT Circ. 2017-08 + 6AMLD"
effectiveFrom = LocalDate.of(2026, 5, 1)
reviewBy = LocalDate.of(2027, 5, 1)
}
// --- DIMENSIONS ---
dimension("client", weight = 0.30) {
score(100) whenMatch { client.pep == PepStatus.OWN }
score(75) whenMatch { client.pep in setOf(PepStatus.FAMILY, PepStatus.CLOSE_ASSOC) }
score(50) whenMatch { client.profession in HIGH_RISK_PROFESSIONS }
score(30) whenMatch { client.residentStatus == NON_RESIDENT }
score(0) otherwise
}
dimension("geo", weight = 0.25) {
score(100) whenMatch { client.country in FATF_BLACKLIST }
score(70) whenMatch { client.country in FATF_GREYLIST_TN_BCT }
score(50) whenMatch { client.country in TRANSPARENCY_CPI_HIGH_CORRUPTION }
score(0) whenMatch { client.country in SAFE_COUNTRIES }
score(20) otherwise
}
dimension("product", weight = 0.20) {
score(80) whenMatch { product.code == "CRYPTO_WALLET" }
score(60) whenMatch { product.code == "COMPTE_DEVISE" && product.expectedMonthly > 50_000 }
score(30) whenMatch { product.code in listOf("COMPTE_COURANT", "LIVRET") }
score(10) whenMatch { product.code == "COMPTE_JEUNE" }
}
dimension("channel", weight = 0.15) {
score(60) whenMatch { session.channel == REMOTE_SELFIE_ONLY }
score(30) whenMatch { session.channel == REMOTE_WITH_QES_TUNTRUST }
score(0) whenMatch { session.channel == FACE_TO_FACE_AGENCY }
}
dimension("aml_screening", weight = 0.10) {
score(100) whenMatch { screening.sanctionsHit == TRUE_POSITIVE }
score(80) whenMatch { screening.adverseMediaScore > 0.8 }
score(50) whenMatch { screening.pepConfirmed }
score(0) otherwise
}
// --- THRESHOLDS ---
threshold(LOW to STANDARD, at = 25)
threshold(STANDARD to HIGH, at = 50)
threshold(HIGH to PROHIBITED, at = 80)
// --- OVERRIDES (priorité > score) ---
mustProhibit whenAny {
screening.ofacMatch == TRUE_POSITIVE
client.country in listOf("KP", "IR")
client.minor && !product.allowsMinor
}
mustHigh whenAny {
client.pep == PepStatus.OWN
client.isLegalRep && entity.ubo.anyPep
screening.adverseMediaScore > 0.9
}
// --- MITIGATIONS par niveau ---
mitigationsFor(HIGH) = listOf(
"Demander justificatif origine des fonds",
"Entretien visio VideoKYC obligatoire",
"Approbation 4-eyes",
"Revue annuelle du profil"
)
mitigationsFor(PROHIBITED) = listOf(
"Refus automatique",
"Alerte compliance officer",
"STR à évaluer sous 48h"
)
}

Le DSL enforce à la compilation :

  • Somme des weight des dimensions doit faire 1.0 (sinon erreur compile)
  • Chaque dimension doit avoir au moins une règle score(...)
  • Les seuils doivent être monotones : LOW→STANDARD < STANDARD→HIGH < HIGH→PROHIBITED
  • Les overrides ne peuvent qu’élever le niveau, jamais l’abaisser (invariant métier)
  • Tout symbole (pays, profession, product code) référencé doit exister dans le catalogue tenant

Le compliance officer n’écrit pas du Kotlin. Il utilise l’UI (workflow 10 mockups) qui sérialise en JSON canonique :

{
"tenant_id": "TN-BANQUEX",
"version": "2.1",
"metadata": { "owner": "compliance-officer@banquex.tn", "description": "..." },
"dimensions": [
{
"name": "client",
"weight": 0.30,
"rules": [
{ "score": 100, "predicate": { "eq": ["client.pep", "OWN"] } },
{ "score": 75, "predicate": { "in": ["client.pep", ["FAMILY", "CLOSE_ASSOC"]] } },
{ "score": 50, "predicate": { "in": ["client.profession", "@HIGH_RISK_PROFESSIONS"] } },
{ "score": 30, "predicate": { "eq": ["client.residentStatus", "NON_RESIDENT"] } },
{ "score": 0, "otherwise": true }
]
}
],
"thresholds": [
{ "from": "LOW", "to": "STANDARD", "at": 25 },
{ "from": "STANDARD", "to": "HIGH", "at": 50 },
{ "from": "HIGH", "to": "PROHIBITED", "at": 80 }
],
"overrides": {
"must_prohibit": [
{ "eq": ["screening.ofacMatch", "TRUE_POSITIVE"] },
{ "in": ["client.country", ["KP", "IR"]] }
],
"must_high": [
{ "eq": ["client.pep", "OWN"] }
]
}
}

Round-trip parfait DSL ↔ JSON : l’UI compile le JSON en DSL Kotlin à la publication.


Chaque évaluation produit un RiskEvaluation :

{
"evaluation_id": "revl_01HWR2X3K8",
"tenant_id": "TN-BANQUEX",
"subject_id": "client:5f3a7b2c",
"subject_kind": "CLIENT_INDIVIDUAL",
"policy_version": "TN-BANQUEX@2.1",
"evaluator_version": "risk-engine@1.4.2",
"evaluated_at": "2026-04-23T11:42:13Z",
"decision": "HIGH",
"global_score": 67.0,
"level": "HIGH",
"path_taken": "score-based",
"dimensions": [
{
"name": "client",
"score": 75.0,
"weight": 0.30,
"contribution": 22.5,
"reasons": [
"client.pep == FAMILY (Mustapha B. — Minister 2019-2021, source Open Sanctions)"
]
},
{
"name": "geo",
"score": 50.0,
"weight": 0.25,
"contribution": 12.5,
"reasons": ["client.country == NG (CPI 24, high corruption — Transparency 2025)"]
},
{
"name": "product",
"score": 60.0,
"weight": 0.20,
"contribution": 12.0,
"reasons": ["product = COMPTE_DEVISE with expectedMonthly=65000 TND (threshold 50k)"]
},
{
"name": "channel",
"score": 60.0,
"weight": 0.15,
"contribution": 9.0,
"reasons": ["session.channel == REMOTE_SELFIE_ONLY (no QES)"]
},
{
"name": "aml_screening",
"score": 50.0,
"weight": 0.10,
"contribution": 5.0,
"reasons": ["screening.pepConfirmed == true (WorldCheck 2026-04-22)"]
}
],
"overrides_triggered": [],
"recommended_action": "EDD",
"required_mitigations": [
"Demander justificatif origine des fonds",
"Entretien visio VideoKYC obligatoire",
"Approbation 4-eyes",
"Revue annuelle du profil"
],
"list_refs": {
"fatf_greylist_version": "2025-10",
"ofac_version": "2026-04-22",
"transparency_cpi_year": 2025
}
}
AudienceChamps lusUsage
Agentdecision, level, dimensions[].reasons, requiredMitigationsComprendre pourquoi et que faire
Compliance officerToutRevoir, overrider (avec justif), prendre décision finale
Auditeur BCT/DGIpolicyVersion, evaluatorVersion, listRefs, evaluatedAt, overridesTriggeredTracer reproductibilité et traçabilité
Client (sur demande RGPD art. 22)level, dimensions[].name + reasons (sanitisés)Droit à l’explication

ComposantChoixRationale
LangageKotlin 1.9+ (JVM 21)Type-safe builders, null-safety, immutability par défaut, écosystème JVM
Runtime DSLKotlin compilation JIT + OPA Rego pour règles complexesKotlin compile le DSL en bytecode à la publication → exécution native
Cache listesCaffeine + refresh async Kafkap99 < 5ms lookup pays/OFAC
Stockage policyPostgreSQL JSONB + trigger d’audit WORMVersioning natif via append-only + timestamp
Évaluations (audit)Table WORM chiffrée au repos (AES-256) + retention 10 ansBCT exige 10 ans, RGPD impose pseudonymisation
StreamKafka topic risk.evaluated (per tenant partition key)Réutilisé par AML, case mgmt, dashboards
Perf targetp50 ≤ 15 ms, p99 ≤ 50 msHot path synchrone sur l’onboarding
POST /v1/risk/policies
Content-Type: application/json
Authorization: Bearer <compliance-officer-token>
{ "tenant_id": "TN-BANQUEX", "draft_of": "TN-BANQUEX@2.0", "dimensions": [...] }
201 Created
{ "policy_id": "pol_01HX...", "version": "draft-2.1", "status": "DRAFT" }
POST /v1/risk/policies/{id}/shadow-activate
→ 200 OK (nouvelle policy évaluée en shadow mode en parallèle de la prod)
POST /v1/risk/policies/{id}/backtest
{ "window_months": 6, "sample_size": 5000 }
202 Accepted (Temporal workflow, résultat via webhook ou polling)
POST /v1/risk/policies/{id}/publish
Content-Type: application/json
{ "dual_control_approver": "dsi-approver@banquex.tn", "approver_signature": "..." }
200 OK (policy devient ACTIVE, l'ancienne passe en ARCHIVED)
POST /v1/risk/evaluate
{
"subject_kind": "CLIENT_INDIVIDUAL",
"client": { ... },
"product": { ... },
"session": { ... },
"screening": { ... }
}
200 OK (RiskEvaluation, cf. §4)
GET /v1/risk/evaluations/{id}
→ 200 OK (RiskEvaluation complet, pour audit)

Pour le scoring temps réel transactionnel (volumétries élevées), gRPC EvaluateTransactional(TxContext) returns (RiskEvaluation) avec streaming bidirectionnel.


Toute publication de policy requiert deux comptes distincts signant électroniquement :

  1. Compliance officer (propose)
  2. Directeur compliance OU DSI (approuve)

Aucune policy ne passe en ACTIVE sans signatures cryptographiques des deux (Ed25519 via Vault), horodatées, attachées à l’audit WORM.

Pendant la phase SHADOW, chaque requête production est évaluée deux fois :

  • Par la policy ACTIVE (décision utilisée en vrai)
  • Par la policy SHADOW (décision logguée uniquement)

Métriques comparées :

MétriqueSeuil d’alerte
% divergence de niveau> 15%
% auto-approve LOW changé> ±10%
% HIGH → PROHIBITED> 5% de nouveaux PROHIBITED
Taux faux-positifs estimé (vs feedback agents)> 2x prod

Si seuils dépassés → la nouvelle policy ne peut pas passer en PUBLISHED sans justif écrite (override du dir compliance).

Temporal workflow :

  1. Charge les N derniers dossiers (par défaut 6 mois, échantillon 5000) depuis la table WORM évaluations.
  2. Ré-évalue chaque dossier avec la policy draft, en tenant compte des listes FATF/OFAC à l’époque de l’évaluation originale (reproductibilité par listRefs).
  3. Produit un rapport :
    • Confusion matrix (vrai niveau vs nouveau niveau)
    • Dossiers dont la décision changerait : top 20 avec raisons
    • Impact business : % auto-approve LOW, SLA agents, charge EDD
    • Impact conformité : % PROHIBITED nouveaux (détection améliorée ?), % dossiers qui passeraient LOW alors qu’avant HIGH (risque régression)
  • Annuelle par défaut (ligne BCT / FATF Reco. 1)
  • Triggerable à tout moment par un compliance officer
  • Rappel automatique 60 / 30 / 7 jours avant échéance
  • Après 1 an sans revue : alerte supervisor + incident compliance

Table risk_policy_audit append-only :

EventChamps obligatoires
policy.createdactor, tenant, content_hash, timestamp
policy.editedactor, policy_id, diff, content_hash, timestamp
policy.shadowedactor, policy_id, timestamp
policy.backtestedactor, policy_id, backtest_report_hash, timestamp
policy.approvedactor, policy_id, signature, timestamp
policy.publishedactor, policy_id, effectiveFrom, timestamp
policy.archivedactor, policy_id, reason, timestamp

Chaîne de hash SHA-256 (chaque entry inclut le hash de l’entry précédente) → intégrité vérifiable par l’auditeur.


MétriqueTargetComment atteint
Évaluation synchrone p50≤ 15 msDSL compilé JIT, listes FATF/OFAC en Caffeine cache (L1), pas de DB hit
Évaluation synchrone p99≤ 50 msidem, plus GC tuning G1GC (pause < 10ms)
Throughput par replica2000 req/sCalcul pur CPU, scale horizontal linéaire
Refresh liste OFAC (quotidien)< 30 s (downtime 0)Read-only pattern + atomic swap de cache
Backtesting 6 mois / 5000 dossiers≤ 15 minTemporal workflow parallèle, batch de 100
Storage evaluations (1 tenant 1M/an)~ 400 GB / an JSONB compresséPartitionnement mensuel + compression lz4

  • Row-Level Security PostgreSQL : tenant_id propagé via SET LOCAL app.tenant_id = '...' dans chaque transaction (cf. ADR-002)
  • Chiffrement at rest : AES-256 sur WORM evaluations + policies
  • Chiffrement in transit : TLS 1.3 + mTLS interne Vault-managed
  • PII handling : le profil client utilisé pour évaluation contient PII → jamais de log raw, seulement subjectId pseudonymisé dans les logs
  • Secret management : clés de signature dual control dans Vault, rotation automatique 90j
  • RBAC :
    • risk.policy.read : tous les officers compliance du tenant
    • risk.policy.draft : senior compliance officers
    • risk.policy.approve : directeur compliance + DSI (un des deux suffit pour signer ; les deux doivent signer pour publier)
    • risk.evaluation.override : senior compliance uniquement, laisse trace audit
  • Anti-tampering : policy content hashé à la création, vérifié à chaque chargement runtime. Mismatch → refus de servir + incident

ModuleIntégrationSens
kyc-svcPOST /v1/risk/evaluate à la soumission onboardingSynchrone, bloquant (hot path)
aml-svc screeningPOST /v1/risk/evaluate après screening, POST /v1/risk/policies/.../publish consomme updated listsSynchrone + event-driven
aml-svc txmongRPC EvaluateTransactional sur chaque transaction eligibleSynchrone temps réel
case-mgmt-svcConsomme Kafka risk.evaluated → alimente queue agent par prioritéAsynchrone
form-designerPublie client_profile_schema consommé par risk engine (nouveau champ form → nouveau variable DSL)Config time
reporting-svcKPI dashboards : % LOW auto-approve, temps moyen EDD, taux PROHIBITEDAsynchrone

VitaKYC livre 4 templates prêts-à-l’emploi comme point de départ :

TemplatePour quiPondérations
TEMPLATE_BANK_TN_BCTBanques tunisiennes (conforme BCT Circ. 2017-08 annexe D)client 0.30 · geo 0.25 · product 0.20 · channel 0.15 · aml 0.10
TEMPLATE_BANK_EU_6AMLDBanques européennes (conforme 6AMLD + BaFin)client 0.25 · geo 0.30 · product 0.15 · channel 0.15 · aml 0.15
TEMPLATE_FINTECH_WALLETFintechs/wallets crypto-friendlyclient 0.20 · geo 0.25 · product 0.25 · channel 0.10 · aml 0.20
TEMPLATE_INSURANCE_CREDITAssurance + crédit (risque produit dominant)client 0.20 · geo 0.15 · product 0.40 · channel 0.10 · aml 0.15

Chaque template est fork-able par tenant, avec notes de politique + model cards.


Chaque dimension a une model card maintenue en YAML dans la policy :

dimension: client
owner: compliance-officer@banquex.tn
purpose: Évaluer le risque intrinsèque lié à la personne (PEP, profession, statut)
variables:
- client.pep (source: screening WorldCheck + OpenSanctions, refresh daily)
- client.profession (source: form KYC, référentiel BCT codes professions)
- client.residentStatus (source: form KYC)
- client.maritalStatus (source: form KYC, optionnel)
regulatory_basis:
- "BCT Circulaire 2017-08 art. 8.2 (risque personne)"
- "FATF Reco. 12 (PEP)"
- "Wolfsberg Group Principles (2015)"
last_review: 2026-04-15
last_reviewer: directeur-compliance@banquex.tn
next_review: 2027-04-15
known_limitations:
- "Adverse media uniquement en FR/AR/EN — gap sur presse locale non-indexée"
- "PEP proche associé : définition subjective, tolérance 10% faux-positifs acceptée"

Avant la 1ère évaluation production :

  • Template de base choisi (ex: TEMPLATE_BANK_TN_BCT)
  • Model cards renseignées pour les 5 dimensions
  • 20+ profils test joués dans le POC, résultats validés par compliance officer
  • Backtesting sur échantillon 6 mois (si historique dispo)
  • Dual control wired (2 comptes avec clés Vault)
  • Shadow mode activé minimum 2 semaines
  • Alertes SLA configurées (drift % auto-approve, p99 évaluation)
  • Export audit WORM testé (format BCT + DGI)
  • Training compliance officers (1 demi-journée par tenant)
  • Playbook calibration publié et signé (voir playbook)