Back to Blog

Financial Risk Ontology Modeling: Real-Time Risk Sensing and Relationship Network Analysis

Financial domain relationship complexity far exceeds other industries:

CoomiaPublished on August 21, 202513 min read
Share this articleTwitter / X

Financial Risk Ontology Modeling: Real-Time Risk Sensing and Relationship Network Analysis

Series: S4 Ontology Modeling · Article 17 | Level: Intermediate | Reading Time: 18 min

#TL;DR

  • Financial risk Ontology centers on the "Customer-Account-Transaction" triad — individuals, corporations, accounts, transactions, products, and risk events form a relationship network. coomia-dip uses RelationType to express complex financial semantics like guarantee relationships, fund flows, and corporate associations.
  • Derived properties enable real-time risk metric computation — customer credit scores, account anomaly detection, and product risk exposure are all auto-computed via derived properties, updating in milliseconds when data changes, eliminating "T+1 risk reporting" delays.
  • Relationship graph traversal discovers hidden risks — 2-3 hop traversals reveal "seemingly unrelated but actually connected" customer clusters, fund cycles, and guarantee chains, delivering risk management capabilities impossible with traditional rule engines.

#1. Unique Challenges of Financial Data

#1.1 Relationship Complexity

Code
Financial domain relationship complexity far exceeds other industries:

Individual customer Zhang San:
├── Holds 3 accounts (savings, credit card, investment)
├── Is legal representative of Company A
├── Is shareholder of Company B (30%)
├── Provided guarantee for Company C
├── Spouse Li Si is also a customer
├── Li Si is legal representative of Company D
├── Company A and Company D have fund transfers
└── Company B provides guarantee for Company A

Problem: Zhang San's loan risk depends not just on personal credit,
but also on all related companies, guarantee chains, and family members.

Traditional approach: Each relationship in different systems, analysis requires manual correlation
coomia-dip: All relationships in unified Ontology model, one query handles everything

#2. Core ObjectType Design

#2.1 Customer Model

Code
ObjectType: Individual
  properties:
    customerId: STRING (PK)
    name: STRING
    idNumber: STRING (SENSITIVE)
    phone: STRING
    email: STRING
    occupation: STRING
    annualIncome: DECIMAL
    riskPreference: ENUM [CONSERVATIVE, MODERATE, AGGRESSIVE]
    kycStatus: ENUM [PENDING, VERIFIED, REJECTED, EXPIRED]
    kycVerifiedAt: TIMESTAMP
    accounts: RELATION[] → Account
    relatedCompanies: RELATION[] → CorporateRelation
    familyMembers: RELATION[] → FamilyRelation
  metrics:
    monthlyTransactionVolume: METRIC(counter, source=core_banking)
    avgDailyBalance: METRIC(gauge, source=core_banking)
    loginFrequency: METRIC(counter, source=mobile_banking)
  derivedProperties:
    creditScore: DERIVED
      expression: |
        baseScore(annualIncome, occupation) * 0.3 +
        paymentHistoryScore(accounts) * 0.35 +
        utilizationScore(accounts) * 0.15 +
        accountAgeScore(accounts) * 0.1 +
        inquiryScore(creditInquiries) * 0.1
    riskLevel: CASE
      WHEN creditScore >= 750 THEN "LOW"
      WHEN creditScore >= 600 THEN "MEDIUM"
      WHEN creditScore >= 400 THEN "HIGH"
      ELSE "VERY_HIGH"
    END
    totalAssets: SUM(Account.balance WHERE accountType IN ("SAVINGS","INVESTMENT"))
    totalLiabilities: SUM(Account.balance WHERE accountType IN ("CREDIT","LOAN"))
    debtToIncomeRatio: totalLiabilities / annualIncome

ObjectType: Corporate
  properties:
    companyId: STRING (PK)
    companyName: STRING
    registrationNumber: STRING
    industry: STRING
    registeredCapital: DECIMAL
    establishedDate: DATE
    legalRepresentative: RELATION → Individual
    shareholders: RELATION[] → ShareholderRelation
    accounts: RELATION[] → Account
    guarantees: RELATION[] → GuaranteeRelation
  metrics:
    revenue: METRIC(counter, source=financial_reports)
    netProfit: METRIC(gauge, source=financial_reports)
    assetTurnoverRatio: METRIC(gauge, source=computed)
  derivedProperties:
    companyRiskScore: DERIVED
      expression: |
        financialHealthScore(revenue, netProfit) * 0.3 +
        industryRiskScore(industry) * 0.2 +
        guaranteeExposure(guarantees) * 0.2 +
        paymentHistoryScore(accounts) * 0.3
    isHighRisk: companyRiskScore < 50
    totalGuaranteeAmount: SUM(GuaranteeRelation.amount)
    guaranteeConcentration: MAX(GuaranteeRelation.amount) / totalGuaranteeAmount

#2.2 Accounts and Transactions

Code
ObjectType: Account
  properties:
    accountId: STRING (PK)
    accountType: ENUM [SAVINGS, CHECKING, CREDIT, LOAN, INVESTMENT, MARGIN]
    owner: RELATION → Individual | Corporate
    currency: STRING
    balance: DECIMAL
    creditLimit: DECIMAL
    interestRate: DECIMAL
    openDate: DATE
    status: ENUM [ACTIVE, FROZEN, CLOSED, DORMANT]
    branch: RELATION → Branch
  metrics:
    dailyTransactionCount: METRIC(counter, source=core_banking)
    dailyTransactionVolume: METRIC(counter, source=core_banking)
    monthlyAvgBalance: METRIC(gauge, source=core_banking)
  derivedProperties:
    utilizationRate: balance / creditLimit (credit accounts)
    isOverLimit: balance > creditLimit
    dormantDays: TODAY() - lastTransactionDate
    isDormant: dormantDays > 180
    averageTransactionSize: dailyTransactionVolume / dailyTransactionCount

ObjectType: Transaction
  properties:
    transactionId: STRING (PK)
    fromAccount: RELATION → Account
    toAccount: RELATION → Account
    amount: DECIMAL
    currency: STRING
    transactionType: ENUM [TRANSFER, PAYMENT, WITHDRAWAL, DEPOSIT, FX, INVESTMENT]
    channel: ENUM [COUNTER, ATM, MOBILE, ONLINE, API]
    timestamp: TIMESTAMP
    status: ENUM [PENDING, COMPLETED, FAILED, REVERSED]
    description: STRING
    location: STRING
    deviceId: STRING
    ipAddress: STRING (SENSITIVE)
  derivedProperties:
    isLargeTransaction: amount > threshold(fromAccount.accountType)
    isCrossBorder: fromAccount.currency != toAccount.currency
    isUnusualTime: HOUR(timestamp) < 6 OR HOUR(timestamp) > 23
    riskScore: DERIVED
      expression: |
        baseRisk(transactionType, amount) +
        (IF(isLargeTransaction, 20, 0)) +
        (IF(isCrossBorder, 15, 0)) +
        (IF(isUnusualTime, 10, 0)) +
        velocityRisk(fromAccount, "1h") +
        locationRisk(location, fromAccount.owner)

#2.3 Risk Relationship Model

Code
RelationType: GuaranteeRelation
  properties:
    guarantor: RELATION → Individual | Corporate
    borrower: RELATION → Individual | Corporate
    guaranteeType: ENUM [JOINT, COLLATERAL, PLEDGE, LETTER_OF_CREDIT]
    amount: DECIMAL
    collateralType: STRING
    collateralValue: DECIMAL
    startDate: DATE
    endDate: DATE
    status: ENUM [ACTIVE, RELEASED, DEFAULTED]

RelationType: ShareholderRelation
  properties:
    shareholder: RELATION → Individual | Corporate
    company: RELATION → Corporate
    shareholdingPercentage: DECIMAL
    shareType: ENUM [COMMON, PREFERRED]
    beneficialOwner: BOOLEAN

RelationType: FamilyRelation
  properties:
    person1: RELATION → Individual
    person2: RELATION → Individual
    relationship: ENUM [SPOUSE, PARENT, CHILD, SIBLING]

RelationType: FundFlow
  properties:
    from: RELATION → Account
    to: RELATION → Account
    totalAmount: DECIMAL
    transactionCount: INT
    timeRange: STRING
    flowType: ENUM [REGULAR, OCCASIONAL, SUSPICIOUS]

#3. Real-Time Risk Metric Computation

#3.1 Customer-Level Risk Dashboard

Code
Individual's risk metric DAG:

Level 0 (source data):
  Transaction records, account balances, personal info, external credit bureau

Level 1 (account-level derived):
  Account.utilizationRate
  Account.overdueDays
  Account.averageTransactionSize

Level 2 (customer-level aggregation):
  Individual.totalAssets = SUM(Account.balance WHERE type IN savings/investment)
  Individual.totalLiabilities = SUM(Account.balance WHERE type IN credit/loan)
  Individual.maxOverdueDays = MAX(Account.overdueDays)

Level 3 (customer-level derived):
  Individual.debtToIncomeRatio = totalLiabilities / annualIncome
  Individual.creditScore = weighted_sum(...)
  Individual.riskLevel = CASE(creditScore)

Level 4 (association risk):
  Individual.guaranteeExposure = SUM(GuaranteeRelation.amount)
  Individual.relatedCompanyRisk = MAX(Corporate.companyRiskScore)

Level 5 (composite risk):
  Individual.compositeRiskScore = creditScore * 0.6 + guaranteeRisk * 0.2 + companyRisk * 0.2

Cascade update on transaction:
  New transaction → balance changes → utilizationRate → creditScore → riskLevel
  Full chain millisecond update, no waiting for T+1 reports

#3.2 Anti-Money Laundering (AML) Metrics

Code
AML-related derived properties:

ObjectType: Individual (AML extensions)
  derivedProperties:
    # Transaction velocity detection
    transactionVelocity_1h: COUNT(Transaction WHERE timestamp > NOW() - 1h)
    transactionVelocity_24h: COUNT(Transaction WHERE timestamp > NOW() - 24h)
    isHighVelocity: transactionVelocity_1h > 10 OR transactionVelocity_24h > 50

    # Structuring / Smurfing detection
    nearThresholdTransactions: COUNT(Transaction WHERE amount BETWEEN 9000 AND 10000 AND timestamp > NOW() - 7d)
    isStructuring: nearThresholdTransactions >= 3

    # Cross-border transaction patterns
    crossBorderFrequency: COUNT(Transaction WHERE isCrossBorder AND timestamp > NOW() - 30d)
    highRiskCountryTransactions: COUNT(Transaction WHERE toAccount.country IN highRiskCountries)

    # Fund cycle detection (via relation traversal)
    hasFundCycle: EXISTS(
      TRAVERSE fromAccount → Transaction.toAccount → Transaction.fromAccount
      WHERE depth <= 3 AND endpoint == startpoint
    )

    # Composite AML risk
    amlRiskScore: DERIVED
      expression: |
        (IF(isHighVelocity, 25, 0)) +
        (IF(isStructuring, 30, 0)) +
        (IF(crossBorderFrequency > 10, 15, 0)) +
        (IF(highRiskCountryTransactions > 0, 20, 0)) +
        (IF(hasFundCycle, 30, 0))

    amlAlert: amlRiskScore >= 50

Alert Action:
  ActionType: CreateSAR (Suspicious Activity Report)
    trigger: amlAlert == true
    parameters:
      customerId: REQUIRED
      alertType: ENUM [STRUCTURING, HIGH_VELOCITY, FUND_CYCLE, HIGH_RISK_COUNTRY]
      assignedAnalyst: RELATION → Employee
      dueDate: NOW() + 5d
    guards:
      - NOT EXISTS(SAR WHERE customerId = this.customerId AND status = "OPEN")

#4. Relationship Graph Analysis

#4.1 Associated Customer Discovery

Code
Discover hidden associations via relationship traversal:

Query: Find all customers associated with "Zhang San"

API: POST /api/v1/objects/Individual/zhang-san/graph
{
  "maxDepth": 3,
  "relationTypes": ["FamilyRelation", "ShareholderRelation", "GuaranteeRelation"],
  "includeProperties": ["name", "creditScore", "riskLevel"]
}

Result:

Zhang San (creditScore: 720, riskLevel: MEDIUM)
├── [Spouse] Li Si (creditScore: 680, riskLevel: MEDIUM)
│   └── [Legal Rep] Company D (riskScore: 45, isHighRisk: true)
│       └── [Fund Transfer] Company A
├── [Legal Rep] Company A (riskScore: 72)
│   └── [Guarantee] ← Company B (riskScore: 55)
├── [Shareholder 30%] Company B (riskScore: 55)
│   └── [Guarantee] → Company A
└── [Guarantor] → Company C (riskScore: 38, isHighRisk: true)

Risk findings:
├── Zhang San's spouse Li Si is linked to high-risk company (Company D)
├── Company A and Company B have mutual guarantee relationship
├── Company C is high-risk, Zhang San guarantees it
└── If Company C defaults, Zhang San bears guarantee liability
    → This affects Zhang San's credit score and loan approval

#4.2 Guarantee Chain Analysis

Code
Guarantee chain risk analysis:

Query: Analyze Company A's guarantee chain

API: POST /api/v1/objects/Corporate/company-a/trace
{
  "relationType": "GuaranteeRelation",
  "direction": "BOTH",
  "maxDepth": 5,
  "includeRiskScores": true
}

Result:

Guarantee chain visualization:
  Company E (riskScore: 82)
    ↓ guarantee $500K
  Company B (riskScore: 55)
    ↓ guarantee $1M
  Company A (riskScore: 72)     ← analysis target
    ↓ guarantee $800K
  Company C (riskScore: 38)     ← HIGH RISK!
    ↓ guarantee $600K
  Company F (riskScore: 25)     ← HIGH RISK!

Risk analysis:
├── Chain depth: 5 layers
├── High-risk entities on chain: 2 (Company C, Company F)
├── Company A's downstream guarantee exposure: $800K (direct) + $600K (indirect) = $1.4M
├── If Company F defaults: Company C must cover $600K
│   → Company C likely cannot cover (riskScore=38)
│   → Company A must cover for Company C: $800K
│   → Cascade risk total: $1.4M
└── Recommendation: Reduce Company A's credit limit, require additional collateral

#4.3 Fund Flow Analysis

Code
Fund flow network analysis:

Query: Analyze fund flow network for Account A001

API: POST /api/v1/objects/Account/A001/fund-flow
{
  "timeRange": {"from": "2025-01-01", "to": "2025-01-31"},
  "minAmount": 100000,
  "maxDepth": 3,
  "detectCycles": true
}

Result:

A001 (Zhang San savings)
├──→ A002 (Company A corporate) $500K x 3 times
│    ├──→ A005 (Company D corporate) $300K x 2 times
│    │    └──→ A003 (Li Si savings) $200K x 1 time
│    │         └──→ A001 (Zhang San savings) $150K x 1 time ← FUND CYCLE!
│    └──→ A006 (Supplier E) $400K x 5 times (normal business)
└──→ A004 (Zhang San credit card) $100K x 2 times (own account)

Findings:
├── Fund cycle: A001 → A002 → A005 → A003 → A001
├── Cycle amount: $150K
├── Suspicion: Funds loop through 4 accounts across 3 related entities
├── Possible purpose: Inflating transaction volume / money laundering / asset transfer
└── Auto-trigger SAR (Suspicious Activity Report)

coomia-dip advantage:
  Traditional requires analysts to manually trace fund flows
  coomia-dip uses relation traversal + cycle detection automatically
  Completes in seconds, covers all accounts

#5. Risk Control Rule Engine

#5.1 Rules Combined with Ontology

Code
Risk control rules implemented via Ontology ActionTypes:

ActionType: FreezeAccount
  trigger:
    OR:
      - account.overdueDays > 90
      - account.owner.amlRiskScore >= 80
      - account.owner.creditScore < 300
      - manual_trigger
  parameters:
    accountId: REQUIRED
    freezeReason: ENUM [OVERDUE, AML_ALERT, FRAUD_SUSPECT, LEGAL_ORDER]
    freezeLevel: ENUM [DEBIT_ONLY, FULL_FREEZE]
    duration: DURATION
  guards:
    - account.status != "FROZEN"
    - account.balance >= 0 OR freezeReason == "LEGAL_ORDER"
  approvalWorkflow:
    - IF freezeLevel == "FULL_FREEZE": require_approval("risk_manager")
    - IF freezeReason == "LEGAL_ORDER": require_approval("compliance_officer")
  sideEffects:
    - NOTIFY(account.owner, "ACCOUNT_FROZEN")
    - UPDATE(account.status, "FROZEN")
    - LOG(auditTrail)

ActionType: AdjustCreditLimit
  trigger:
    OR:
      - creditScore change exceeds 50 points
      - debtToIncomeRatio > 0.6
      - guaranteeExposure increased
  parameters:
    accountId: REQUIRED
    newLimit: DECIMAL
    adjustmentReason: STRING
  guards:
    - newLimit >= 0
    - newLimit <= maxAllowedLimit(account.owner.creditScore)

#6. Credit Approval Model

Code
ObjectType: LoanApplication
  properties:
    applicationId: STRING (PK)
    applicant: RELATION → Individual | Corporate
    loanType: ENUM [PERSONAL, MORTGAGE, AUTO, BUSINESS, CREDIT_LINE]
    requestedAmount: DECIMAL
    requestedTerm: INT (months)
    purpose: STRING
    collateral: RELATION[] → Collateral
    guarantors: RELATION[] → Individual
    status: ENUM [SUBMITTED, UNDER_REVIEW, APPROVED, REJECTED, DISBURSED]
    submittedAt: TIMESTAMP
    decisionAt: TIMESTAMP
    assignedOfficer: RELATION → Employee
  derivedProperties:
    autoDecision: DERIVED
      expression: |
        CASE
          WHEN applicant.creditScore >= 750 AND requestedAmount <= preApprovedLimit THEN "AUTO_APPROVE"
          WHEN applicant.creditScore < 400 THEN "AUTO_REJECT"
          WHEN applicant.amlRiskScore >= 50 THEN "MANUAL_REVIEW"
          WHEN applicant.debtToIncomeRatio > 0.5 THEN "MANUAL_REVIEW"
          WHEN guaranteeChainRisk > 0.7 THEN "MANUAL_REVIEW"
          ELSE "MANUAL_REVIEW"
        END

    guaranteeChainRisk: DERIVED
      expression: |
        TRAVERSE(applicant, "GuaranteeRelation", depth=3)
        → Count high-risk entities and their proportion on the chain

    expectedLossRate: DERIVED
      expression: |
        PD(applicant.creditScore) * LGD(collateral) * EAD(requestedAmount)

    riskAdjustedPricing: DERIVED
      expression: |
        baseRate + riskPremium(expectedLossRate) + operatingCostRate

#7. Regulatory Compliance Model

Code
ObjectType: RegulatoryReport
  properties:
    reportId: STRING (PK)
    reportType: ENUM [CAR, LCR, NSFR, LR, SAR, CTR]
    reportingPeriod: STRING
    status: ENUM [DRAFT, SUBMITTED, ACCEPTED, REJECTED]
    submittedAt: TIMESTAMP
    dueDate: DATE
  derivedProperties:
    capitalAdequacyRatio: DERIVED
      expression: |
        (tier1Capital + tier2Capital) / riskWeightedAssets * 100

    liquidityCoverageRatio: DERIVED
      expression: |
        highQualityLiquidAssets / totalNetCashOutflows30d * 100

    nonPerformingLoanRatio: DERIVED
      expression: |
        COUNT(Account WHERE overdueDays > 90 AND accountType = "LOAN") /
        COUNT(Account WHERE accountType = "LOAN") * 100

All regulatory metrics are Ontology derived properties:
├── Data computed in real-time, always shows latest values
├── Historical values auto-saved for trend analysis
├── Data lineage traceable to every transaction
└── Audit can precisely reproduce metric values at any point in time

#8. Fraud Detection Model

Code
ObjectType: FraudAlert
  properties:
    alertId: STRING (PK)
    alertType: ENUM [IDENTITY_THEFT, CARD_FRAUD, ACCOUNT_TAKEOVER,
                     APPLICATION_FRAUD, INSIDER_FRAUD]
    relatedTransaction: RELATION → Transaction
    relatedAccount: RELATION → Account
    relatedCustomer: RELATION → Individual
    confidence: DECIMAL
    status: ENUM [OPEN, INVESTIGATING, CONFIRMED, FALSE_POSITIVE]
    assignedAnalyst: RELATION → Employee

Fraud detection derived properties (real-time):

Transaction fraud score:
  fraudScore: DERIVED
    expression: |
      deviceRisk(deviceId, account.knownDevices) * 0.2 +
      locationRisk(location, account.normalLocations) * 0.2 +
      amountRisk(amount, account.averageTransactionSize) * 0.2 +
      velocityRisk(account, "1h") * 0.2 +
      behaviorRisk(transactionType, channel, timestamp) * 0.2

  isFraudSuspect: fraudScore >= 70

Alert rules:
  When isFraudSuspect == true:
  1. Block transaction in real-time (if fraudScore >= 90)
  2. Send SMS verification (if fraudScore 70-90)
  3. Create FraudAlert record
  4. Notify anti-fraud analyst
  5. Freeze related accounts (if confirmed fraud)

#9. Relationship Network Overview

Code
Financial risk Ontology relationship network:

Individual ──holds──→ Account
Corporate ──holds──→ Account
Individual ──represents──→ Corporate
Individual ──shareholderOf──→ Corporate
Individual ──guarantees──→ Corporate
Individual ──familyOf──→ Individual
Corporate ──guarantees──→ Corporate
Corporate ──subsidiaryOf──→ Corporate
Account ──transfers──→ Account (via Transaction)
Individual ──applies──→ LoanApplication
LoanApplication ──securedBy──→ Collateral
FraudAlert ──relatedTo──→ Transaction
FraudAlert ──involves──→ Account
SAR ──reportedFor──→ Individual

Total relation types: 14
Total ObjectTypes: 12

Core value:
  Traditional risk: Rule-based, looks at individual data only
  coomia-dip risk: Network-based, sees individual + associations + contagion
  "Zhang San is low-risk, but Company C he guarantees is high-risk"
  → This association risk is only discoverable through relationship graphs

#10. Comparison with Traditional Risk Management

Code
Ontology risk management vs traditional:

┌──────────────────┬────────────────────┬──────────────────────┐
│ Dimension        │ coomia-dip          │ Traditional Risk     │
├──────────────────┼────────────────────┼──────────────────────┤
│ Data model       │ Unified Ontology   │ Multi-system silos   │
│ Relationship     │ Graph traversal    │ SQL JOIN, 2-3 tables │
│ Metric compute   │ Real-time derived  │ T+1 batch compute    │
│ Rule execution   │ ActionType + Guard │ Separate rule engine │
│ Cycle detection  │ Built-in graph algo│ Requires graph DB    │
│ Regulatory rpt   │ Auto derived props │ Manual data extract  │
│ Audit trail      │ Property lineage   │ Report-level trace   │
│ Extensibility    │ Add ObjectType     │ Alter tables/fields  │
│ Dev efficiency   │ Schema-driven      │ Requires dev + test  │
└──────────────────┴────────────────────┴──────────────────────┘

#Key Takeaways

  1. Financial Ontology's core is the relationship network, not isolated entities — a customer's risk depends not only on their own data but on guarantee chains, equity relationships, family ties, and fund flows. coomia-dip RelationType naturally supports this complex relationship modeling.
  2. Derived properties enable "real-time risk management" — credit scores, AML metrics, and fraud scores are all auto-computed via derived properties, updating in milliseconds on transactions, upgrading from "T+1 risk reports" to "real-time risk sensing."
  3. Relationship graph traversal is Ontology risk management's unique capability — 2-3 hop traversals discover hidden associated customers, fund cycles, and guarantee chain risks that traditional SQL JOINs and rule engines cannot achieve.
  4. ActionTypes close the loop from "detecting risk" to "handling risk" — account freezing, limit adjustment, SAR submission, and loan rejection all execute through Ontology Action mechanisms with approval workflows and audit logs.
  5. Regulatory compliance metrics are a natural application of derived properties — capital adequacy ratio, liquidity coverage ratio, and NPL ratio are auto-computed through Ontology, with data traceable to every transaction for audit requirements.

#Next Article

The next article S4-18 Healthcare Ontology Modeling discusses how to express the healthcare domain's complex semantics using the Ontology model — patients, encounters, diagnoses, prescriptions, lab results, and medical devices — how to handle healthcare data privacy compliance, and how relationship networks enable disease spectrum analysis.

#ontology #financial-risk #aml #credit-scoring #fraud-detection #guarantee-chain #fund-flow #compliance #graph-analysis