Разработка системы transaction monitoring для AML
Transaction monitoring (TM) — непрерывный процесс анализа транзакций для выявления подозрительной активности. Это не просто блеклист проверка: TM выявляет паттерны (structuring, rapid movement, round-trip) которые по отдельности выглядят нормально, но в совокупности — признак отмывания.
Архитектура TM системы
Два подхода: rule-based (детерминированные правила) и ML-based (anomaly detection). В production используют оба: правила для известных паттернов, ML для обнаружения нового.
Rule Engine
interface MonitoringRule {
id: string;
name: string;
category: "structuring" | "velocity" | "geographic" | "behavioral" | "watchlist";
evaluate: (context: TransactionContext) => Promise<RuleResult>;
alertLevel: AlertLevel;
action: AlertAction;
}
class TransactionMonitoringEngine {
private rules: MonitoringRule[];
async evaluateTransaction(tx: Transaction): Promise<EvaluationResult> {
const context = await this.buildContext(tx);
const results = await Promise.all(
this.rules.map(rule => rule.evaluate(context).catch(err => ({
triggered: false,
error: err.message,
})))
);
const triggered = results.filter(r => r.triggered);
if (triggered.length === 0) return { action: "ALLOW", ruleHits: [] };
const maxLevel = Math.max(...triggered.map(r => r.alertLevel));
const action = this.determineAction(maxLevel, triggered);
if (action !== "ALLOW") {
await this.createAlert(tx, triggered, action);
}
return { action, ruleHits: triggered };
}
private async buildContext(tx: Transaction): Promise<TransactionContext> {
const [history30d, history24h, userProfile, walletRisk] = await Promise.all([
this.db.getTransactionHistory(tx.userId, 30),
this.db.getTransactionHistory(tx.userId, 1),
this.db.getUserProfile(tx.userId),
this.amlProvider.getWalletRisk(tx.address, tx.asset),
]);
return { transaction: tx, history30d, history24h, userProfile, walletRisk };
}
}
Конкретные правила
const STRUCTURING_RULE: MonitoringRule = {
id: "TM-001",
name: "Structuring Detection",
category: "structuring",
alertLevel: AlertLevel.HIGH,
action: AlertAction.FREEZE_AND_REVIEW,
async evaluate(ctx: TransactionContext): Promise<RuleResult> {
const REPORTING_THRESHOLD = 10000;
// Находим транзакции чуть ниже threshold за 3 дня
const nearThreshold = ctx.history30d.filter(t =>
t.usdAmount >= REPORTING_THRESHOLD * 0.7 &&
t.usdAmount < REPORTING_THRESHOLD &&
Date.now() - t.timestamp < 3 * 86400000
);
const currentNearThreshold = ctx.transaction.usdAmount >= REPORTING_THRESHOLD * 0.7 &&
ctx.transaction.usdAmount < REPORTING_THRESHOLD;
if (currentNearThreshold && nearThreshold.length >= 2) {
return {
triggered: true,
alertLevel: AlertLevel.HIGH,
details: `${nearThreshold.length + 1} transactions just below $${REPORTING_THRESHOLD}`,
evidence: nearThreshold.map(t => t.id),
};
}
return { triggered: false };
},
};
const VELOCITY_RULE: MonitoringRule = {
id: "TM-002",
name: "Unusual Velocity",
category: "velocity",
alertLevel: AlertLevel.MEDIUM,
action: AlertAction.ENHANCED_MONITORING,
async evaluate(ctx: TransactionContext): Promise<RuleResult> {
const avg30d = ctx.history30d.reduce((sum, t) => sum + t.usdAmount, 0) / 30;
const total24h = ctx.history24h.reduce((sum, t) => sum + t.usdAmount, 0) +
ctx.transaction.usdAmount;
const velocityRatio = avg30d > 0 ? total24h / avg30d : total24h > 500 ? 999 : 0;
if (velocityRatio >= 10) {
return {
triggered: true,
alertLevel: AlertLevel.MEDIUM,
details: `24h volume ${total24h.toFixed(0)} USD is ${velocityRatio.toFixed(1)}x daily average`,
};
}
return { triggered: false };
},
};
const ROUND_TRIP_RULE: MonitoringRule = {
id: "TM-003",
name: "Round Trip Detection",
category: "behavioral",
alertLevel: AlertLevel.HIGH,
action: AlertAction.FREEZE_AND_REVIEW,
async evaluate(ctx: TransactionContext): Promise<RuleResult> {
// Деньги пришли и ушли через несколько адресов обратно
if (ctx.transaction.type !== "WITHDRAWAL") return { triggered: false };
const recentDeposits = ctx.history30d.filter(t =>
t.type === "DEPOSIT" &&
Math.abs(t.usdAmount - ctx.transaction.usdAmount) < ctx.transaction.usdAmount * 0.05 && // ±5%
Date.now() - t.timestamp < 7 * 86400000
);
if (recentDeposits.length > 0) {
return {
triggered: true,
alertLevel: AlertLevel.HIGH,
details: `Similar deposit ${recentDeposits[0].usdAmount} USD received ${
Math.round((Date.now() - recentDeposits[0].timestamp) / 3600000)} hours ago`,
};
}
return { triggered: false };
},
};
ML-based Anomaly Detection
Для продвинутых систем — Isolation Forest или Autoencoder для выявления аномалий:
from sklearn.ensemble import IsolationForest
import numpy as np
class TransactionAnomalyDetector:
def __init__(self):
self.model = IsolationForest(contamination=0.01, random_state=42)
def extract_features(self, transaction, user_history):
return [
transaction['usd_amount'],
transaction['usd_amount'] / (user_history['avg_30d'] + 1),
len(user_history['transactions_24h']),
transaction['hour_of_day'],
transaction['day_of_week'],
user_history['unique_counterparties_7d'],
transaction['aml_risk_score'],
]
def predict(self, features) -> float:
# Returns: -1 anomaly, 1 normal
# Transform to probability
score = self.model.score_samples([features])[0]
return (score + 0.5) * 2 # normalize to [0, 1]
Alert Management и SAR
class AlertManager {
async createAlert(tx: Transaction, rules: RuleResult[], action: AlertAction): Promise<Alert> {
const alert = await this.db.createAlert({
transactionId: tx.id,
userId: tx.userId,
triggeredRules: rules.map(r => r.ruleId),
maxAlertLevel: Math.max(...rules.map(r => r.alertLevel)),
action,
status: AlertStatus.OPEN,
assignedTo: await this.autoAssignCompliance(),
dueDate: this.calculateDueDate(action),
});
if (action === AlertAction.FREEZE_AND_REVIEW) {
await this.freezeUserAccount(tx.userId, alert.id);
}
await this.notifyComplianceTeam(alert);
return alert;
}
async resolveSARAlert(alertId: string, sarDecision: SARDecision): Promise<void> {
if (sarDecision.submitSAR) {
await this.sarService.createAndSubmit({
alertId,
suspiciousActivity: sarDecision.description,
supportingTransactions: sarDecision.transactions,
});
}
await this.db.updateAlert(alertId, {
status: sarDecision.submitSAR ? AlertStatus.SAR_SUBMITTED : AlertStatus.CLOSED,
resolution: sarDecision.resolution,
resolvedAt: new Date(),
});
}
}
Стек
| Компонент | Технология |
|---|---|
| Rule engine | Node.js + TypeScript |
| ML detection | Python + scikit-learn или TensorFlow |
| Streaming | Apache Kafka для real-time |
| Storage | PostgreSQL + TimescaleDB (time-series) |
| Alerting | Custom + PagerDuty |
| Dashboard | React |
Полная TM система с rule engine, ML аномалиями и SAR management: 2-3 месяца.







