blob: e3ba0014ff3321b448ab09c4ac22bb0edc3129ab [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.ignite.scalar.examples
import org.apache.ignite.scalar.scalar
import org.apache.ignite.scalar.scalar._
import scala.util.Random
import scala.util.control.Breaks._
/**
* Scalar-based Monte-Carlo example.
* <p/>
* Remote nodes should always be started with special configuration file which
* enables P2P class loading: `'ignite.{sh|bat} examples/config/example-ignite.xml'`.
* <p/>
* Alternatively you can run `ExampleNodeStartup` in another JVM which will
* start node with `examples/config/example-ignite.xml` configuration.
*/
object ScalarCreditRiskExample {
def main(args: Array[String]) {
scalar("examples/config/example-ignite.xml") {
// Create portfolio.
var portfolio = Seq.empty[Credit]
val rnd = new Random
// Generate some test portfolio items.
(0 until 5000).foreach(i =>
portfolio +:= Credit(
50000 * rnd.nextDouble,
rnd.nextInt(1000),
rnd.nextDouble / 10,
rnd.nextDouble / 20 + 0.02
)
)
// Forecast horizon in days.
val horizon = 365
// Number of Monte-Carlo iterations.
val iter = 10000
// Percentile.
val percentile = 0.95
// Mark the stopwatch.
val start = System.currentTimeMillis
// Calculate credit risk and print it out.
// As you can see the ignite cluster enabling is completely hidden from the caller
// and it is fully transparent to him. In fact, the caller is never directly
// aware if method was executed just locally or on the 100s of cluster nodes.
// Credit risk crdRisk is the minimal amount that creditor has to have
// available to cover possible defaults.
val crdRisk = ignite$ @< (closures(ignite$.cluster().nodes().size(), portfolio.toArray, horizon, iter, percentile),
(s: Seq[Double]) => s.sum / s.size, null)
println("Credit risk [crdRisk=" + crdRisk + ", duration=" +
(System.currentTimeMillis - start) + "ms]")
}
}
/**
* Creates closures for calculating credit risks.
*
* @param clusterSize Size of the cluster.
* @param portfolio Portfolio.
* @param horizon Forecast horizon in days.
* @param iter Number of Monte-Carlo iterations.
* @param percentile Percentile.
* @return Collection of closures.
*/
private def closures(clusterSize: Int, portfolio: Array[Credit], horizon: Int, iter: Int,
percentile: Double): Seq[() => Double] = {
val iterPerNode: Int = math.round(iter / clusterSize.asInstanceOf[Float])
val lastNodeIter: Int = iter - (clusterSize - 1) * iterPerNode
var cls = Seq.empty[() => Double]
(0 until clusterSize).foreach(i => {
val nodeIter = if (i == clusterSize - 1) lastNodeIter else iterPerNode
cls +:= (() => new CreditRiskManager().calculateCreditRiskMonteCarlo(
portfolio, horizon, nodeIter, percentile))
})
cls
}
}
/**
* This class provides a simple model for a credit contract (or a loan). It is basically
* defines as remaining crediting amount to date, credit remaining term, APR and annual
* probability on default. Although this model is simplified for the purpose
* of this example, it is close enough to emulate the real-life credit
* risk assessment application.
*/
private case class Credit(
remAmnt: Double, // Remaining crediting amount.
remTerm: Int, // Remaining crediting remTerm.
apr: Double, // Annual percentage rate (APR).
edf: Double // Expected annual probability of default (EaDF).
) {
/**
* Gets either credit probability of default for the given period of time
* if remaining term is less than crediting time or probability of default
* for whole remained crediting time.
*
* @param term Default term.
* @return Credit probability of default in relative percents
* (percentage / 100).
*/
def getDefaultProbability(term: Int): Double = {
(1 - math.exp(math.log(1 - edf) * math.min(remTerm, term) / 365.0))
}
}
/**
* This class abstracts out the calculation of risk for a credit portfolio.
*/
private class CreditRiskManager {
/**
* Default randomizer with normal distribution.
* Note that since every JVM on the ignite cluster will have its own random
* generator (independently initialized) the Monte-Carlo simulation
* will be slightly skewed when performed on the ignite cluster due to skewed
* normal distribution of the sub-jobs comparing to execution on the
* local node only with single random generator. Real-life applications
* may want to provide its own implementation of distributed random
* generator.
*/
private val rndGen = new Random
/**
* Calculates credit risk for a given credit portfolio. This calculation uses
* Monte-Carlo Simulation to produce risk value.
*
* @param portfolio Credit portfolio.
* @param horizon Forecast horizon (in days).
* @param num Number of Monte-Carlo iterations.
* @param percentile Cutoff level.
* @return Credit risk value, i.e. the minimal amount that creditor has to
* have available to cover possible defaults.
*/
def calculateCreditRiskMonteCarlo(portfolio: Seq[Credit], horizon: Int, num:
Int, percentile: Double): Double = {
println(">>> Calculating credit risk for portfolio [size=" + portfolio.length + ", horizon=" +
horizon + ", percentile=" + percentile + ", iterations=" + num + "] <<<")
val start = System.currentTimeMillis
val losses = calculateLosses(portfolio, horizon, num).sorted
val lossProbs = new Array[Double](losses.size)
(0 until losses.size).foreach(i => {
if (i == 0)
lossProbs(i) = getLossProbability(losses, 0)
else if (losses(i) != losses(i - 1))
lossProbs(i) = getLossProbability(losses, i) + lossProbs(i - 1)
else
lossProbs(i) = lossProbs(i - 1)
})
var crdRisk = 0.0
breakable {
(0 until lossProbs.size).foreach(i => {
if (lossProbs(i) > percentile) {
crdRisk = losses(i - 1)
break()
}
})
}
println(">>> Finished calculating portfolio risk [risk=" + crdRisk +
", time=" + (System.currentTimeMillis - start) + "ms]")
crdRisk
}
/**
* Calculates losses for the given credit portfolio using Monte-Carlo Simulation.
* Simulates probability of default only.
*
* @param portfolio Credit portfolio.
* @param horizon Forecast horizon.
* @param num Number of Monte-Carlo iterations.
* @return Losses array simulated by Monte Carlo method.
*/
private def calculateLosses(portfolio: Seq[Credit], horizon: Int, num: Int): Array[Double] = {
val losses = new Array[Double](num)
// Count losses using Monte-Carlo method. We generate random probability of default,
// if it exceeds certain credit default value we count losses - otherwise count income.
(0 until num).foreach(i => {
portfolio.foreach(crd => {
val remDays = math.min(crd.remTerm, horizon)
if (rndGen.nextDouble >= 1 - crd.getDefaultProbability(remDays))
// (1 + 'r' * min(H, W) / 365) * S.
// Where W is a horizon, H is a remaining crediting term, 'r' is an annual credit rate,
// S is a remaining credit amount.
losses(i) += (1 + crd.apr * math.min(horizon, crd.remTerm) / 365) * crd.remAmnt
else
// - 'r' * min(H,W) / 365 * S
// Where W is a horizon, H is a remaining crediting term, 'r' is a annual credit rate,
// S is a remaining credit amount.
losses(i) -= crd.apr * math.min(horizon, crd.remTerm) / 365 * crd.remAmnt
})
})
losses
}
/**
* Calculates probability of certain loss in array of losses.
*
* @param losses Array of losses.
* @param i Index of certain loss in array.
* @return Probability of loss with given index.
*/
private def getLossProbability(losses: Array[Double], i: Int): Double = {
var count = 0.0
losses.foreach(tmp => {
if (tmp == losses(i))
count += 1
})
count / losses.size
}
}