version: 2
title: Financial Risk Recipe
description: |-
  The financial industry's current approach to managing mandated operational risk capital
  requirements, batch processing, often leads to over- or under-allocation of certain
  classes of funds, operating with tight time constraints, and slow reactions to changing
  market conditions.

  By responding to market changes in real time, organizations can provide adequate coverage
  for risk exposure while ensuring their compliance minimally affects their asset allocation.

  The intent of this recipe is to show an example of conditionally adjusting data (investment
  value) based on a property (investment class) of the manifested nodes prior to aggregating the
  value at multiple levels.  Further, the adjusted aggregates are used to alert on threshold
  crossing (percentage of value of specific classes).

  This is accomplished via three technical strategies:

  1. Use of `NumberIterator` to generate sample transactions
  2. Conditional handling of data
  3. Real-time graph-based data (from #2) aggregated across multiple levels

ingestStreams:
  - name: generate-finance-data
    source:
      type: NumberIterator
      startOffset: 0
      limit: 1
    query: |-
      WITH 0 AS institutionId
      // Generate 10 desks - change the range bound to alter the number of generated desks
      UNWIND range(1, 10) AS deskId
      MATCH (institution), (desk)
      WHERE id(institution) = idFrom('institution', institutionId)
          AND id(desk) = idFrom('desk', institutionId, deskId)

      SET institution:institution

      SET desk:desk,
          desk.deskNumber = deskId

      CREATE (institution)-[:HAS]->(desk)

      WITH *
      // Generate 1000 investments per desk- change the range bound to alter the number of investments generated per desk
      UNWIND range(1, 1000) AS investmentId
      MATCH (investment)
      WHERE id(investment) = idFrom('investment', institutionId, deskId, investmentId)

      SET investment:investment,
          investment.investmentId = toInteger(toString(deskId) + toString(investmentId)),
          investment.type = toInteger(rand() * 10) + 1,
          investment.code = gen.string.from(strId(investment), 25),
          investment.value = gen.float.from(strId(investment)) * 100

      WITH id(investment) AS invId, desk, investment
      CALL {
            WITH invId
            MATCH (investment:investment)
            WHERE id(investment) = invId
            SET investment.class = CASE
              WHEN investment.type <= 5 THEN '1'
              WHEN investment.type >= 6 AND investment.type <= 8 THEN '2a'
              WHEN investment.type >= 9 THEN '2b'
            END

            RETURN investment.type AS type
          }

      CREATE (desk)-[:HOLDS]->(investment)

standingQueries:
  - pattern:
      type: Cypher
      query: |-
        MATCH (investment:investment)<-[:HOLDS]-(desk:desk)<-[:HAS]-(institution:institution)
        RETURN DISTINCT id(investment) AS id
      mode: DistinctId
    outputs:
      - name: adjustValues
        preEnrichmentTransformation:
          type: InlineData
        resultEnrichment:
          query: |-
            MATCH (investment:investment)<-[:HOLDS]-(desk:desk)<-[:HAS]-(institution:institution)
            WHERE id(investment) = $that.data.id

            SET investment.adjustedValue = CASE
                  WHEN investment.class = '1' THEN investment.value
                  WHEN investment.class = '2a' THEN investment.value * .85
                  WHEN investment.class = '2b' AND investment.type = 9 THEN investment.value * .75
                  WHEN investment.class = '2b' AND investment.type = 10 THEN investment.value * .5
                END
          parameter: that
        destinations:
          - type: Drop

  - pattern:
      type: Cypher
      query: |-
        MATCH (investment:investment)<-[:HOLDS]-(desk:desk)<-[:HAS]-(institution:institution)
        WHERE investment.adjustedValue IS NOT NULL
        RETURN DISTINCT id(investment) AS id
      mode: DistinctId
    outputs:
      - name: rollUps
        preEnrichmentTransformation:
          type: InlineData
        resultEnrichment:
          query: |-
            MATCH (investment)<-[:HOLDS]-(desk:desk)<-[:HAS]-(institution:institution)
            WHERE id(investment) = $that.data.id
              AND investment.adjustedValue IS NOT NULL

            UNWIND [["1","adjustedValue1"], ["2a","adjustedValue2a"], ["2b","adjustedValue2b"]] AS stuff

            WITH institution,investment,desk,stuff
            WHERE investment.class = stuff[0]

            CALL float.add(institution,stuff[1],investment.adjustedValue) YIELD result AS institutionAdjustedValueRollupByClass
            CALL float.add(institution,"totalAdjustedValue",investment.adjustedValue) YIELD result AS institutionAdjustedValueRollup

            CALL float.add(desk,stuff[1],investment.adjustedValue) YIELD result AS deskAdjustedValueRollupByClass
            CALL float.add(desk,"totalAdjustedValue",investment.adjustedValue) YIELD result AS deskAdjustedValueRollup

            SET institution.percentAdjustedValue2 = ((institution.adjustedValue2a + institution.adjustedValue2b)/institution.totalAdjustedValue) * 100,
                institution.percentAdjustedValue2b = (institution.adjustedValue2b/institution.totalAdjustedValue) * 100
          parameter: that
        destinations:
          - type: Drop

  - pattern:
      type: Cypher
      query: |-
        MATCH (investment:investment)<-[:HOLDS]-(desk:desk)<-[:HAS]-(institution:institution)
        RETURN DISTINCT id(investment) AS id
      mode: DistinctId
    outputs:
      - name: class2CompositionAlert
        preEnrichmentTransformation:
          type: InlineData
        resultEnrichment:
          query: |-
            MATCH (investment:investment)<-[:HOLDS]-(desk:desk)<-[:HAS]-(institution:institution)
            WHERE id(investment) = $that.data.id
              AND (institution.investments = 2500 OR institution.investments = 5000 OR institution.investments = 10000)
              AND institution.percentAdjustedValue2 > 40

            RETURN institution.percentAdjustedValue2 AS Class_2_Composition
          parameter: that
        destinations:
          - type: StandardOut
      - name: class2bCompositionAlert
        preEnrichmentTransformation:
          type: InlineData
        resultEnrichment:
          query: |-
            MATCH (investment:investment)<-[:HOLDS]-(desk:desk)<-[:HAS]-(institution:institution)
            WHERE id(investment) = $that.data.id
              AND (institution.investments = 2500 OR institution.investments = 5000 OR institution.investments = 10000)
              AND institution.percentAdjustedValue2b > 15

            RETURN institution.percentAdjustedValue2b AS Class_2b_Composition
          parameter: that
        destinations:
          - type: StandardOut
quickQueries:
  - predicate:
      propertyKeys: []
      knownValues: {}
    quickQuery:
      name: "[Node] Adjacent Nodes"
      querySuffix: MATCH (n)--(m) RETURN DISTINCT m
      sort:
        type: Node
  - predicate:
      propertyKeys: []
      knownValues: {}
    quickQuery:
      name: "[Node] Parent Node"
      querySuffix: MATCH (n)<-[]-(m) RETURN DISTINCT m
      sort:
        type: Node
  - predicate:
      propertyKeys: []
      knownValues: {}
    quickQuery:
      name: "[Node] Refresh"
      querySuffix: RETURN n
      sort:
        type: Node
  - predicate:
      propertyKeys: []
      knownValues: {}
    quickQuery:
      name: "[Text] Local Properties"
      querySuffix: RETURN id(n) AS NODE_ID, labels(n) AS NODE_LABELS, properties(n) AS NODE_PROPERTIES
      sort:
        type: Text
  - predicate:
      propertyKeys: []
      knownValues: {}
    quickQuery:
      name: "[Text] Node Label"
      querySuffix: RETURN labels(n)
      sort:
        type: Text

sampleQueries:
  - name: Last 10 Nodes
    query: CALL recentNodes(10)
  - name: Legend (show one of each node type)
    query: MATCH (n) WHERE labels(n) IS NOT NULL WITH labels(n) AS kind, collect(n) AS legend RETURN legend[0]
  - name: Show distribution of investment node classes (grouped by desk)
    query: MATCH (investment:investment)<-[]-(desk:desk) RETURN desk.deskNumber AS DESK, investment.investmentId AS INVESTMENT, investment.class AS CLASS ORDER BY desk.deskNumber
  - name: Wiretap Standing Query 1
    query: 'CALL standing.wiretap({ name: "STANDING-1"}) YIELD meta, data WHERE meta.isPositiveMatch MATCH (n) WHERE id(n) = data.id RETURN properties(n)'
  - name: Wiretap Standing Query 2
    query: 'CALL standing.wiretap({ name: "STANDING-2"}) YIELD meta, data WHERE meta.isPositiveMatch MATCH (n) WHERE id(n) = data.id RETURN properties(n)'
  - name: Wiretap Standing Query 3
    query: 'CALL standing.wiretap({ name: "STANDING-3"}) YIELD meta, data WHERE meta.isPositiveMatch MATCH (n) WHERE id(n) = data.id RETURN properties(n)'

nodeAppearances:
  - predicate:
      propertyKeys:
        - type
      knownValues:
        type: 1
      dbLabel: investment
    icon: ion-cash
    color: "#85BB65"
    size:
    label:
      type: Property
      key: investmentId
      prefix: "Investment ID: "
  - predicate:
      propertyKeys:
        - type
      knownValues:
        type: 2
      dbLabel: investment
    icon: ion-cash
    color: "#85BB65"
    size:
    label:
      type: Property
      key: investmentId
      prefix: "Investment ID: "
  - predicate:
      propertyKeys:
        - type
      knownValues:
        type: 3
      dbLabel: investment
    icon: ion-cash
    color: "#85BB65"
    size:
    label:
      type: Property
      key: investmentId
      prefix: "Investment ID: "
  - predicate:
      propertyKeys:
        - type
      knownValues:
        type: 4
      dbLabel: investment
    icon: ion-cash
    color: "#85BB65"
    size:
    label:
      type: Property
      key: investmentId
      prefix: "Investment ID: "
  - predicate:
      propertyKeys:
        - type
      knownValues:
        type: 5
      dbLabel: investment
    icon: ion-cash
    color: "#85BB65"
    size:
    label:
      type: Property
      key: investmentId
      prefix: "Investment ID: "
  - predicate:
      propertyKeys:
        - type
      knownValues:
        type: 6
      dbLabel: investment
    icon: ion-android-warning
    color: "#FFAA33"
    size:
    label:
      type: Property
      key: investmentId
      prefix: "Investment ID: "
  - predicate:
      propertyKeys:
        - type
      knownValues:
        type: 7
      dbLabel: investment
    icon: ion-android-warning
    color: "#FFAA33"
    size:
    label:
      type: Property
      key: investmentId
      prefix: "Investment ID: "
  - predicate:
      propertyKeys:
        - type
      knownValues:
        type: 8
      dbLabel: investment
    icon: ion-android-warning
    color: "#FFAA33"
    size:
    label:
      type: Property
      key: investmentId
      prefix: "Investment ID: "
  - predicate:
      propertyKeys:
        - type
      knownValues:
        type: 9
      dbLabel: investment
    icon: ion-android-alert
    color: "#880808"
    size:
    label:
      type: Property
      key: investmentId
      prefix: "Investment ID: "
  - predicate:
      propertyKeys:
        - type
      knownValues:
        type: 10
      dbLabel: investment
    icon: ion-android-alert
    color: "#880808"
    size:
    label:
      type: Property
      key: investmentId
      prefix: "Investment ID: "
  - predicate:
      propertyKeys: []
      knownValues: {}
      dbLabel: desk
    icon: ion-archive
    color: "#aaa9ad"
    size:
    label:
      type: Property
      key: deskNumber
      prefix: "Desk: "
  - predicate:
      propertyKeys: []
      knownValues: {}
      dbLabel: institution
    icon: ion-android-home
    color: "#AA4A44"
    size:
