diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000..c1dd12f Binary files /dev/null and b/.mvn/wrapper/maven-wrapper.jar differ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..b74bf7f --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar diff --git a/mvnw b/mvnw new file mode 100755 index 0000000..8a8fb22 --- /dev/null +++ b/mvnw @@ -0,0 +1,316 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# 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 +# +# https://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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`\\unset -f command; \\command -v java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..1d8ab01 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,188 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..6731a97 --- /dev/null +++ b/pom.xml @@ -0,0 +1,78 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.7.5 + + + com.theodo.dojo + unit-test-dojo + 0.0.1-SNAPSHOT + unit-test-dojo + A DOJO to learn Unit Test Best Practices + + 11 + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + 2.13.0 + test + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + 2.7.5 + test + + + com.intellij + annotations + 9.0.4 + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + diff --git a/src/main/java/com/theodo/dojo/unittestdojo/UnitTestDojoApplication.java b/src/main/java/com/theodo/dojo/unittestdojo/UnitTestDojoApplication.java new file mode 100644 index 0000000..56af743 --- /dev/null +++ b/src/main/java/com/theodo/dojo/unittestdojo/UnitTestDojoApplication.java @@ -0,0 +1,13 @@ +package com.theodo.dojo.unittestdojo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class UnitTestDojoApplication { + + public static void main(String[] args) { + SpringApplication.run(UnitTestDojoApplication.class, args); + } + +} diff --git a/src/main/java/com/theodo/dojo/unittestdojo/pricer/cashflows/CashFlows.java b/src/main/java/com/theodo/dojo/unittestdojo/pricer/cashflows/CashFlows.java new file mode 100644 index 0000000..cb1895e --- /dev/null +++ b/src/main/java/com/theodo/dojo/unittestdojo/pricer/cashflows/CashFlows.java @@ -0,0 +1,46 @@ +package com.theodo.dojo.unittestdojo.pricer.cashflows; + +import lombok.Builder; +import lombok.Getter; + +import java.util.*; + +import static com.theodo.dojo.unittestdojo.utils.DateUtils.daysBetween; + +@Builder +@Getter +public class CashFlows { + + + + @Builder + @Getter + public static class CashFlow implements Comparable{ + private final Date date; + private final double cashFlowValue; + + public int compareTo(CashFlow o) { + return date.compareTo(o.date); + } + } + private final List sortedCashFlows = new ArrayList<>(); + + public void addCashFlow(CashFlow... cashFlows){ + this.sortedCashFlows.addAll(Arrays.asList(cashFlows)); + Collections.sort(this.sortedCashFlows); + } + + public Double[] computeMaturitiesAtDate(Date currentDate){ + return sortedCashFlows.stream() + .filter(cashFlow -> cashFlow.date.compareTo(currentDate) > 0) + .map(cashFlow -> (daysBetween(currentDate, cashFlow.date)) / 365.) + .toArray(Double[]::new); + } + + public Double[] filterCashFlowsAfterDate(Date currentDate) { + return sortedCashFlows.stream() + .filter(cashFlow -> cashFlow.date.compareTo(currentDate) > 0) + .map(CashFlow::getCashFlowValue) + .toArray(Double[]::new); + } +} diff --git a/src/main/java/com/theodo/dojo/unittestdojo/pricer/shift/ShiftProvider.java b/src/main/java/com/theodo/dojo/unittestdojo/pricer/shift/ShiftProvider.java new file mode 100644 index 0000000..2c02c82 --- /dev/null +++ b/src/main/java/com/theodo/dojo/unittestdojo/pricer/shift/ShiftProvider.java @@ -0,0 +1,5 @@ +package com.theodo.dojo.unittestdojo.pricer.shift; + +public interface ShiftProvider { + double getCurrentShift(); +} diff --git a/src/main/java/com/theodo/dojo/unittestdojo/pricer/spread/SpreadEvaluation.java b/src/main/java/com/theodo/dojo/unittestdojo/pricer/spread/SpreadEvaluation.java new file mode 100644 index 0000000..be383b9 --- /dev/null +++ b/src/main/java/com/theodo/dojo/unittestdojo/pricer/spread/SpreadEvaluation.java @@ -0,0 +1,41 @@ +package com.theodo.dojo.unittestdojo.pricer.spread; + +import com.theodo.dojo.unittestdojo.pricer.cashflows.CashFlows; +import com.theodo.dojo.unittestdojo.pricer.shift.ShiftProvider; +import com.theodo.dojo.unittestdojo.pricer.spread.algorithm.Search; +import com.theodo.dojo.unittestdojo.pricer.yieldcurves.YieldCurve; +import com.theodo.dojo.unittestdojo.pricer.yieldcurves.YieldCurveProvider; +import lombok.RequiredArgsConstructor; + +import java.util.Date; + +import static com.theodo.dojo.unittestdojo.pricer.yieldcurves.YieldCurveInterpolation.getInterpolatedCurveAtMaturities; + +@RequiredArgsConstructor +public class SpreadEvaluation { + + private final CashFlows cashFlows; + private final YieldCurveProvider curveProvider; + private final ShiftProvider shiftProvider; + + public static SpreadEvaluation createCalculator(CashFlows cashFlows, YieldCurveProvider curveProvider, ShiftProvider shiftProvider) { + return new SpreadEvaluation(cashFlows, curveProvider, shiftProvider); + } + + public double searchSpread(Date currentDate, double currentPrice, String curveName) throws Exception { + YieldCurve curve = curveProvider.getYieldCurveByName(curveName); + if(curve == null) throw new Exception("Curve '" + curveName + "' not found in curve repository"); + + Double[] maturities = cashFlows.computeMaturitiesAtDate(currentDate); + Double[] prices = cashFlows.filterCashFlowsAfterDate(currentDate); + + YieldCurve interpolatedCurveAtMaturities = getInterpolatedCurveAtMaturities(maturities, curve); + Double[] yields = interpolatedCurveAtMaturities.getSortedPoints().stream().map(YieldCurve.CurvePoint::getYieldAtMaturity).toArray(Double[]::new); + + if(yields.length != maturities.length){ + throw new Exception("Curve " + curveName + " has not enough points to cover the cash flows maturities"); + } + return Search.init(maturities, prices, yields, shiftProvider.getCurrentShift()).search(currentPrice); + } + +} diff --git a/src/main/java/com/theodo/dojo/unittestdojo/pricer/spread/algorithm/Search.java b/src/main/java/com/theodo/dojo/unittestdojo/pricer/spread/algorithm/Search.java new file mode 100644 index 0000000..4e5944d --- /dev/null +++ b/src/main/java/com/theodo/dojo/unittestdojo/pricer/spread/algorithm/Search.java @@ -0,0 +1,64 @@ +package com.theodo.dojo.unittestdojo.pricer.spread.algorithm; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class Search { + private final Double[] maturities; + private final Double[] prices; + private final Double[] yields; + + private final double currentShift; + + private double expectedPrice; + + private int count = 0; + + public static Search init(Double[] maturities, Double[] prices, Double[] yields, double currentShift) { + return new Search(maturities, prices, yields, currentShift); + } + + public double search(double expectedPrice) throws Exception { + this.expectedPrice = expectedPrice; + this.count = 0; + + return searchBestResult(-1.0, 10.0); + } + + private double searchBestResult(double minSpread, double maxSpread) throws Exception { + double spread = (minSpread + maxSpread) / 2.0; + double price = computePrice(spread); + + if (Math.abs(expectedPrice - price) < 1.0E-8) { + return spread; + } + + if (price < expectedPrice) { + return searchBestResult(minSpread, spread); + } + return searchBestResult(spread, maxSpread); + } + + private double computePrice(double spread) throws Exception { + double price = 0.; + for (int i = 0; i < prices.length; i++) { + double cashFlowPrice = prices[i]; + double maturity = maturities[i]; + double yield = yields[i]; + + double tmp = 1.0 + yield + spread + currentShift; + if (tmp < 0.) { + return Double.NaN; + } + + price += cashFlowPrice / Math.pow(tmp, maturity); + } + + if (count == 500) { + throw new Exception("Calculator was not able to find a spread using convergence method"); + } + count++; + + return price; + } +} diff --git a/src/main/java/com/theodo/dojo/unittestdojo/pricer/yieldcurves/YieldCurve.java b/src/main/java/com/theodo/dojo/unittestdojo/pricer/yieldcurves/YieldCurve.java new file mode 100644 index 0000000..edf4346 --- /dev/null +++ b/src/main/java/com/theodo/dojo/unittestdojo/pricer/yieldcurves/YieldCurve.java @@ -0,0 +1,34 @@ +package com.theodo.dojo.unittestdojo.pricer.yieldcurves; + +import lombok.Builder; +import lombok.Getter; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +@Builder +@Getter +public class YieldCurve { + + @Builder + @Getter + public static class CurvePoint implements Comparable{ + private final double maturity; + private final double yieldAtMaturity; + + @Override + public int compareTo(CurvePoint o) { + return Double.compare(maturity, o.maturity); + } + } + + private final String yieldCurveName; + private final List sortedPoints = new ArrayList<>(); + + public void addPoints(CurvePoint... curvePoints){ + this.sortedPoints.addAll(Arrays.asList(curvePoints)); + Collections.sort(sortedPoints); + } +} diff --git a/src/main/java/com/theodo/dojo/unittestdojo/pricer/yieldcurves/YieldCurveInterpolation.java b/src/main/java/com/theodo/dojo/unittestdojo/pricer/yieldcurves/YieldCurveInterpolation.java new file mode 100644 index 0000000..4dc559c --- /dev/null +++ b/src/main/java/com/theodo/dojo/unittestdojo/pricer/yieldcurves/YieldCurveInterpolation.java @@ -0,0 +1,51 @@ +package com.theodo.dojo.unittestdojo.pricer.yieldcurves; + +import java.util.Arrays; +import java.util.List; + +public class YieldCurveInterpolation { + public static YieldCurve getInterpolatedCurveAtMaturities(Double[] requestedMaturities, YieldCurve sourceCurve) { + YieldCurve interpolatedCurve = YieldCurve.builder().yieldCurveName("Interpolated curve from source curve named: " + sourceCurve.getYieldCurveName()).build(); + if (sourceCurve.getSortedPoints().isEmpty()) { + return interpolatedCurve; + } + Arrays.sort(requestedMaturities); + + List sortedPoints = sourceCurve.getSortedPoints(); + for (double requestedMaturity : requestedMaturities) { + createInterpolatedPointAtMaturity(interpolatedCurve, sortedPoints, requestedMaturity); + } + return interpolatedCurve; + } + + private static void createInterpolatedPointAtMaturity(YieldCurve interpolatedCurve, List sortedPoints, double requestedMaturity) { + YieldCurve.CurvePoint previousPoint = sortedPoints.get(0); + for (YieldCurve.CurvePoint currentPoint : sortedPoints) { + if (currentPoint.getMaturity() >= requestedMaturity) { + interpolatedCurve.addPoints(interpolate(previousPoint, currentPoint, requestedMaturity)); + return; + } + previousPoint = currentPoint; + } + + YieldCurve.CurvePoint currentPoint = sortedPoints.get(sortedPoints.size() - 1); + interpolatedCurve.addPoints(interpolate(previousPoint, currentPoint, requestedMaturity)); + } + + private static YieldCurve.CurvePoint interpolate(YieldCurve.CurvePoint pointA, + YieldCurve.CurvePoint pointB, + double requestedMaturity) { + if (pointB.getMaturity() == pointA.getMaturity()) { + return YieldCurve.CurvePoint.builder() + .maturity(requestedMaturity) + .yieldAtMaturity(pointA.getYieldAtMaturity()) + .build(); + } + + double alpha = (pointB.getYieldAtMaturity() - pointA.getYieldAtMaturity()) / (pointB.getMaturity() - pointA.getMaturity()); + return YieldCurve.CurvePoint.builder() + .maturity(requestedMaturity) + .yieldAtMaturity(alpha * (requestedMaturity - pointA.getMaturity()) + pointA.getYieldAtMaturity()) + .build(); + } +} diff --git a/src/main/java/com/theodo/dojo/unittestdojo/pricer/yieldcurves/YieldCurveProvider.java b/src/main/java/com/theodo/dojo/unittestdojo/pricer/yieldcurves/YieldCurveProvider.java new file mode 100644 index 0000000..af4fb64 --- /dev/null +++ b/src/main/java/com/theodo/dojo/unittestdojo/pricer/yieldcurves/YieldCurveProvider.java @@ -0,0 +1,5 @@ +package com.theodo.dojo.unittestdojo.pricer.yieldcurves; + +public interface YieldCurveProvider { + YieldCurve getYieldCurveByName(String curveName); +} diff --git a/src/main/java/com/theodo/dojo/unittestdojo/utils/DateUtils.java b/src/main/java/com/theodo/dojo/unittestdojo/utils/DateUtils.java new file mode 100644 index 0000000..5e36071 --- /dev/null +++ b/src/main/java/com/theodo/dojo/unittestdojo/utils/DateUtils.java @@ -0,0 +1,40 @@ +package com.theodo.dojo.unittestdojo.utils; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; + +public class DateUtils { + public static final double MILLISECONDS_IN_ONE_DAY = 86400000D; + private static final SimpleDateFormat SIMPLE_DATE_FORMAT_DD_MM_YYYY = new SimpleDateFormat("dd/MM/yyyy"); + + public static int daysBetween(Date infDate, Date supDate) { + double deltaTime = supDate.getTime() - infDate.getTime(); + return (int)round(deltaTime / MILLISECONDS_IN_ONE_DAY); + } + + public static Date parseDate(String ddmmyyyy) { + if (ddmmyyyy == null) { + return null; + } + if ("null".equals(ddmmyyyy)) { + return null; + } + try { + synchronized (SIMPLE_DATE_FORMAT_DD_MM_YYYY) { + return SIMPLE_DATE_FORMAT_DD_MM_YYYY.parse(ddmmyyyy); + } + } + catch (ParseException e) { + throw new RuntimeException(e); + } + } + private static long round(double num) { + if (num >= 0) { + return (long)(num + 0.5); + } + else { + return -round(-num); + } + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/src/test/java/com/theodo/dojo/unittestdojo/pricer/cashflows/CashFlowParser.java b/src/test/java/com/theodo/dojo/unittestdojo/pricer/cashflows/CashFlowParser.java new file mode 100644 index 0000000..9eba937 --- /dev/null +++ b/src/test/java/com/theodo/dojo/unittestdojo/pricer/cashflows/CashFlowParser.java @@ -0,0 +1,67 @@ +package com.theodo.dojo.unittestdojo.pricer.cashflows; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.theodo.dojo.unittestdojo.utils.DateUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class CashFlowParser { + + public static class CashFlowsByName extends HashMap {} + public static class CashFlowsDeserializer extends StdDeserializer { + + protected CashFlowsDeserializer() { + super(CashFlowsByName.class); + } + + @Override + public CashFlowsByName deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { + JsonNode node = jsonParser.getCodec().readTree(jsonParser); + ArrayNode cashFlowsNode = (ArrayNode) node.get("cashFlows"); + CashFlowsByName outputs = new CashFlowsByName(); + for (int i = 0; i < cashFlowsNode.size(); i++) { + JsonNode cashFlowNode = cashFlowsNode.get(i); + String isin = cashFlowNode.get("isin").asText(); + ArrayNode amounts = (ArrayNode) cashFlowNode.get("amounts"); + CashFlows cashFlows = CashFlows.builder().build(); + List values = new ArrayList<>(); + for (int j = 0; j < amounts.size(); j++) { + JsonNode pointNode = amounts.get(j); + values.add(CashFlows.CashFlow.builder() + .cashFlowValue(pointNode.get("value").asDouble()) + .date(DateUtils.parseDate(pointNode.get("date").asText())) + .build()); + } + cashFlows.addCashFlow(values.toArray(new CashFlows.CashFlow[0])); + outputs.put(isin, cashFlows); + } + return outputs; + } + } + + public static Map readFile(String curveResourceName){ + ObjectMapper mapper = new ObjectMapper(new JsonFactory()); + + SimpleModule module = new SimpleModule(); + module.addDeserializer(CashFlowsByName.class, new CashFlowsDeserializer()); + mapper.registerModule(module); + + try(InputStream resource = CashFlowParser.class.getClassLoader().getResourceAsStream(curveResourceName)) { + return mapper.readValue(resource, CashFlowsByName.class); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/test/java/com/theodo/dojo/unittestdojo/pricer/cashflows/CashFlowsTest.java b/src/test/java/com/theodo/dojo/unittestdojo/pricer/cashflows/CashFlowsTest.java new file mode 100644 index 0000000..97a782f --- /dev/null +++ b/src/test/java/com/theodo/dojo/unittestdojo/pricer/cashflows/CashFlowsTest.java @@ -0,0 +1,48 @@ +package com.theodo.dojo.unittestdojo.pricer.cashflows; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Map; +import java.util.stream.Stream; + +import static com.theodo.dojo.unittestdojo.utils.DateUtils.parseDate; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +class CashFlowsTest { + + private static final String TOTAL_ISIN = "FR0000120271"; + private static final double ERROR_MARGIN = 0.01; + private Map cashFlowsByIsin; + + @BeforeEach + public void prepareCashFlows() { + cashFlowsByIsin = CashFlowParser.readFile("cashFlows/cashFlows.json"); + } + + @ParameterizedTest + @MethodSource("provideDatesAndExpectedMaturities") + public void testComputeMaturitiesAtGivenDates(String currentDate, double[] expectedMaturities) { + CashFlows cashFlows = cashFlowsByIsin.get(TOTAL_ISIN); + + Double[] maturitiesAtDate = cashFlows.computeMaturitiesAtDate(parseDate(currentDate)); + + assertArrayEquals(expectedMaturities, toPrimitiveArray(maturitiesAtDate), ERROR_MARGIN); + } + + private static Stream provideDatesAndExpectedMaturities() { + return Stream.of( + Arguments.of("12/01/2000", new double[]{1., 2., 3., 4.}), + Arguments.of("12/07/2000", new double[]{0.5, 1.5, 2.5, 3.5}), + Arguments.of("31/12/2000", new double[]{0.03, 1.03, 2.03, 3.03}) + ); + } + + private static double[] toPrimitiveArray(Double[] values) { + return Stream.of(values).mapToDouble(Double::doubleValue).toArray(); + } + +} + diff --git a/src/test/java/com/theodo/dojo/unittestdojo/pricer/spread/SpreadEvaluationTest.java b/src/test/java/com/theodo/dojo/unittestdojo/pricer/spread/SpreadEvaluationTest.java new file mode 100644 index 0000000..0e3710e --- /dev/null +++ b/src/test/java/com/theodo/dojo/unittestdojo/pricer/spread/SpreadEvaluationTest.java @@ -0,0 +1,114 @@ +package com.theodo.dojo.unittestdojo.pricer.spread; + +import com.theodo.dojo.unittestdojo.pricer.cashflows.CashFlowParser; +import com.theodo.dojo.unittestdojo.pricer.cashflows.CashFlows; +import com.theodo.dojo.unittestdojo.pricer.shift.ShiftProvider; +import com.theodo.dojo.unittestdojo.pricer.yieldcurves.YieldCurve; +import com.theodo.dojo.unittestdojo.pricer.yieldcurves.YieldCurveParser; +import com.theodo.dojo.unittestdojo.pricer.yieldcurves.YieldCurveProvider; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Map; +import java.util.stream.Stream; + +import static com.theodo.dojo.unittestdojo.utils.DateUtils.parseDate; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + + +@TestMethodOrder(MethodOrderer.Random.class) +class SpreadEvaluationTest { + private static final String TOTAL_ISIN = "FR0000120271"; + private static final double ERROR_MARGIN = 0.000001; + private static double SHIFT = 0.005; + private Map yieldCurves; + private Map cashFlowsByIsin; + + @BeforeEach + public void prepareData() { + yieldCurves = YieldCurveParser.readFile("yieldCurves/curves.json"); + cashFlowsByIsin = CashFlowParser.readFile("cashFlows/cashFlowsForPricerTests.json"); + + SHIFT = 0; + } + + @ParameterizedTest + @MethodSource("provideCurveExpectedPriceAndDate") + public void testSpreadCalculator(String curveName, Double price, String currentDate, Double expectedSpread) throws Exception { + CashFlows cashFlows = cashFlowsByIsin.get(TOTAL_ISIN); + + SpreadEvaluation calculator = SpreadEvaluation.createCalculator(cashFlows, curveProvider(), getShiftProvider()); + double spread = calculator.searchSpread(parseDate(currentDate), price, curveName); + assertEquals(expectedSpread, spread, ERROR_MARGIN, "Check Expected Spread computed"); + } + + @Test + public void testSpreadCalculatorWithShift() throws Exception { + SHIFT = 0.05; + CashFlows cashFlows = cashFlowsByIsin.get(TOTAL_ISIN); + + + SpreadEvaluation calculator = SpreadEvaluation.createCalculator(cashFlows, curveProvider(), getShiftProvider()); + double spread = calculator.searchSpread(parseDate("01/06/2004"), 200, "Euro Curve"); + + double expectedSpread = -0.0740670; + assertEquals(expectedSpread, spread, ERROR_MARGIN, "Check Expected Spread computed"); + } + + + @Test + public void testNonConvergence() { + CashFlows cashFlows = cashFlowsByIsin.get(TOTAL_ISIN); + + SpreadEvaluation calculator = SpreadEvaluation.createCalculator(cashFlows, curveProvider(), getShiftProvider()); + Exception exception = assertThrows(Exception.class, + () -> calculator.searchSpread(parseDate("01/06/2006"), 400, "Euro Curve")); + + assertEquals("Calculator was not able to find a spread using convergence method", exception.getMessage()); + } + + @Test + public void testCurveDoesNotExist() { + CashFlows cashFlows = cashFlowsByIsin.get(TOTAL_ISIN); + + SpreadEvaluation calculator = SpreadEvaluation.createCalculator(cashFlows, curveProvider(), getShiftProvider()); + Exception exception = assertThrows(Exception.class, + () -> calculator.searchSpread(parseDate("01/06/2006"), 100, "Unknown Curve")); + + assertEquals("Curve 'Unknown Curve' not found in curve repository", exception.getMessage()); + } + + private static Stream provideCurveExpectedPriceAndDate() { + return Stream.of( + Arguments.of("Euro Curve", 127., "01/06/2004", 0.524166), + Arguments.of("Euro Curve", 130., "01/06/2004", 0.488248), + Arguments.of("Euro Curve", 131., "01/06/2004", 0.476680), + Arguments.of("Euro Curve", 144., "01/06/2004", 0.342330), + + Arguments.of("Euro Curve", 127., "01/06/2005", -0.326832), + Arguments.of("Euro Curve", 130., "01/06/2005", -0.353709), + Arguments.of("Euro Curve", 131., "01/06/2005", -0.3623115), + Arguments.of("Euro Curve", 144., "01/06/2005", -0.4601620), + + Arguments.of("Flat Curve", 127., "01/06/2004", 0.5249149), + Arguments.of("Flat Curve", 130., "01/06/2004", 0.4890030), + Arguments.of("Flat Curve", 131., "01/06/2004", 0.47743742), + Arguments.of("Flat Curve", 144., "01/06/2004", 0.3431120) + ); + } + + private YieldCurveProvider curveProvider() { + return curveName -> yieldCurves.get(curveName); + } + + private static ShiftProvider getShiftProvider() { + return () -> SHIFT; + } + +} diff --git a/src/test/java/com/theodo/dojo/unittestdojo/pricer/yieldcurves/CurveAsserter.java b/src/test/java/com/theodo/dojo/unittestdojo/pricer/yieldcurves/CurveAsserter.java new file mode 100644 index 0000000..4791b12 --- /dev/null +++ b/src/test/java/com/theodo/dojo/unittestdojo/pricer/yieldcurves/CurveAsserter.java @@ -0,0 +1,41 @@ +package com.theodo.dojo.unittestdojo.pricer.yieldcurves; + +import lombok.RequiredArgsConstructor; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +@RequiredArgsConstructor +public class CurveAsserter { + private static final double EPSILON = 1E-10; + private final YieldCurve yieldCurveToTest; + + public static CurveAsserter assertThat(YieldCurve yieldCurveToTest){ + return new CurveAsserter(yieldCurveToTest); + } + + public CurveAsserter isNamed(String expectedName){ + assertEquals(expectedName, yieldCurveToTest.getYieldCurveName()); + return this; + } + + public CurveAsserter sizeIs(int expectedSize){ + assertEquals(expectedSize, yieldCurveToTest.getSortedPoints().size(), "Check number of points is: " + expectedSize); + return this; + } + + public CurveAsserter isEmpty(){ + return sizeIs(0); + } + public CurveAsserter valueAtMaturityIsEqualTo(double expectedMaturity, double expectedValue) { + for (YieldCurve.CurvePoint sortedPoint : yieldCurveToTest.getSortedPoints()) { + if(Math.abs(sortedPoint.getMaturity() - expectedMaturity) < EPSILON){ + assertEquals(expectedValue, sortedPoint.getYieldAtMaturity(), EPSILON); + return this; + } + } + fail("Expected Maturity:" + expectedMaturity + " not found in curve"); + return this; + } + +} diff --git a/src/test/java/com/theodo/dojo/unittestdojo/pricer/yieldcurves/YieldCurveInterpolationTest.java b/src/test/java/com/theodo/dojo/unittestdojo/pricer/yieldcurves/YieldCurveInterpolationTest.java new file mode 100644 index 0000000..b3776cf --- /dev/null +++ b/src/test/java/com/theodo/dojo/unittestdojo/pricer/yieldcurves/YieldCurveInterpolationTest.java @@ -0,0 +1,71 @@ +package com.theodo.dojo.unittestdojo.pricer.yieldcurves; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static com.theodo.dojo.unittestdojo.pricer.yieldcurves.CurveAsserter.assertThat; +import static com.theodo.dojo.unittestdojo.pricer.yieldcurves.YieldCurveInterpolation.getInterpolatedCurveAtMaturities; + +class YieldCurveInterpolationTest { + private Map yieldCurves; + + @BeforeEach + public void prepareCurves() { + yieldCurves = YieldCurveParser.readFile("yieldCurves/curves.json"); + } + + @Test + public void testInterpolationOnFlatCurve() { + YieldCurve flatCurve = yieldCurves.get("Flat Curve"); + YieldCurve interpolatedCurve = getInterpolatedCurveAtMaturities(new Double[]{0., 3., 7., 12.}, flatCurve); + + assertThat(interpolatedCurve) + .isNamed("Interpolated curve from source curve named: Flat Curve") + .sizeIs(4) + .valueAtMaturityIsEqualTo( 0., 0.05) + .valueAtMaturityIsEqualTo( 3., 0.05) + .valueAtMaturityIsEqualTo( 7., 0.05) + .valueAtMaturityIsEqualTo( 12., 0.05); + } + + @Test + public void testInterpolationOnCurve() { + YieldCurve euroCurve = yieldCurves.get("Euro Curve"); + + YieldCurve interpolatedCurve = getInterpolatedCurveAtMaturities(new Double[]{0., 3., 4.49555099247, 7., 8., 12.}, euroCurve); + + double interpolatedValueAt3Years = (3. - 2.49691991786) * (0.065 - 0.06) / (3.49623545517 - 2.49691991786) + 0.06; + double interpolatedValueAt8Years = (8. - 7.49349760438) * (0.08 - 0.07) / (8.49281314168 - 7.49349760438) + 0.07; + + assertThat(interpolatedCurve) + .sizeIs(6) + .valueAtMaturityIsEqualTo( 0., 0.05) + .valueAtMaturityIsEqualTo( 3., interpolatedValueAt3Years) + .valueAtMaturityIsEqualTo( 4.49555099247, 0.065) + .valueAtMaturityIsEqualTo( 7., 0.07) + .valueAtMaturityIsEqualTo( 8., interpolatedValueAt8Years) + .valueAtMaturityIsEqualTo( 12., 0.08); + } + + @Test + public void testInterpolateEmptyCurve() { + YieldCurve emptyCurve = yieldCurves.get("Empty Curve"); + + YieldCurve interpolatedCurve = getInterpolatedCurveAtMaturities(new Double[]{0., 3., 7., 8., 12.}, emptyCurve); + + assertThat(interpolatedCurve).isEmpty(); + } + + @Test + public void testInterpolateNoMaturity() { + YieldCurve euroCurve = yieldCurves.get("Euro Curve"); + + YieldCurve interpolatedCurve = getInterpolatedCurveAtMaturities(new Double[]{}, euroCurve); + + assertThat(interpolatedCurve).isEmpty(); + } +} + + diff --git a/src/test/java/com/theodo/dojo/unittestdojo/pricer/yieldcurves/YieldCurveParser.java b/src/test/java/com/theodo/dojo/unittestdojo/pricer/yieldcurves/YieldCurveParser.java new file mode 100644 index 0000000..61d2738 --- /dev/null +++ b/src/test/java/com/theodo/dojo/unittestdojo/pricer/yieldcurves/YieldCurveParser.java @@ -0,0 +1,62 @@ +package com.theodo.dojo.unittestdojo.pricer.yieldcurves; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.node.ArrayNode; + +import java.io.IOException; +import java.io.InputStream; +import java.util.*; + +public class YieldCurveParser { + + public static class Curves extends HashMap {} + public static class YieldCurveDeserializer extends StdDeserializer { + + protected YieldCurveDeserializer() { + super(Curves.class); + } + @Override + public Curves deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { + JsonNode node = jsonParser.getCodec().readTree(jsonParser); + ArrayNode curvesNode = (ArrayNode) node.get("curves"); + Curves outputs = new Curves(); + for (int i = 0; i < curvesNode.size(); i++) { + JsonNode curveNode = curvesNode.get(i); + String curveName = curveNode.get("name").asText(); + ArrayNode points = (ArrayNode) curveNode.get("points"); + YieldCurve yieldCurve = YieldCurve.builder().yieldCurveName(curveName).build(); + List curvePoints = new ArrayList<>(); + for (int j = 0; j < points.size(); j++) { + JsonNode pointNode = points.get(j); + curvePoints.add(YieldCurve.CurvePoint.builder() + .yieldAtMaturity(pointNode.get("yield").asDouble()) + .maturity(pointNode.get("maturity").asDouble()) + .build()); + } + yieldCurve.addPoints(curvePoints.toArray(new YieldCurve.CurvePoint[0])); + outputs.put(yieldCurve.getYieldCurveName(), yieldCurve); + } + return outputs; + } + } + + public static Map readFile(String curveResourceName){ + ObjectMapper mapper = new ObjectMapper(new JsonFactory()); + + SimpleModule module = new SimpleModule(); + module.addDeserializer(Curves.class, new YieldCurveDeserializer()); + mapper.registerModule(module); + + try(InputStream resource = YieldCurveParser.class.getClassLoader().getResourceAsStream(curveResourceName)) { + return mapper.readValue(resource, Curves.class); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/test/resources/cashFlows/cashFlows.json b/src/test/resources/cashFlows/cashFlows.json new file mode 100644 index 0000000..36cd4b5 --- /dev/null +++ b/src/test/resources/cashFlows/cashFlows.json @@ -0,0 +1,54 @@ +{ + "cashFlows": [ + { + "isin": "FR0000120271", + "amounts": [ + { + "value": "100.", + "date": "12/01/2000" + }, + { + "value": "106.", + "date": "12/01/2001" + }, + { + "value": "104.", + "date": "12/01/2002" + }, + { + "value": "99.", + "date": "12/01/2003" + }, + { + "value": "101.", + "date": "12/01/2004" + } + ] + }, + { + "isin": "GB0007980591", + "amounts": [ + { + "value": "123.", + "date": "12/01/2000" + }, + { + "value": "124.", + "date": "12/01/2001" + }, + { + "value": "99.", + "date": "12/01/2002" + }, + { + "value": "97.", + "date": "12/01/2003" + }, + { + "value": "80.", + "date": "12/01/2004" + } + ] + } + ] +} diff --git a/src/test/resources/cashFlows/cashFlowsForPricerTests.json b/src/test/resources/cashFlows/cashFlowsForPricerTests.json new file mode 100644 index 0000000..47447ce --- /dev/null +++ b/src/test/resources/cashFlows/cashFlowsForPricerTests.json @@ -0,0 +1,62 @@ +{ + "cashFlows": [ + { + "isin": "FR0000120271", + "amounts": [ + { + "value": "100.", + "date": "12/01/2000" + }, + { + "value": "106.", + "date": "12/01/2001" + }, + { + "value": "104.", + "date": "12/01/2002" + }, + { + "value": "99.", + "date": "12/01/2003" + }, + { + "value": "101.", + "date": "12/01/2004" + }, + { + "value": "102.", + "date": "12/01/2005" + }, + { + "value": "104.", + "date": "12/01/2006" + } + ] + }, + { + "isin": "GB0007980591", + "amounts": [ + { + "value": "123.", + "date": "12/01/2000" + }, + { + "value": "124.", + "date": "12/01/2001" + }, + { + "value": "99.", + "date": "12/01/2002" + }, + { + "value": "97.", + "date": "12/01/2003" + }, + { + "value": "80.", + "date": "12/01/2004" + } + ] + } + ] +} diff --git a/src/test/resources/yieldCurves/curves.json b/src/test/resources/yieldCurves/curves.json new file mode 100644 index 0000000..4edcc15 --- /dev/null +++ b/src/test/resources/yieldCurves/curves.json @@ -0,0 +1,102 @@ +{ + "curves": [ + { + "name": "Flat Curve", + "points": [ + { + "maturity": "0.49828884326", + "yield": "0.05" + }, + { + "maturity": "1.49760438056", + "yield": "0.05" + }, + { + "maturity": "2.49691991786", + "yield": "0.05" + }, + { + "maturity": "3.49623545517", + "yield": "0.05" + }, + { + "maturity": "4.49555099247", + "yield": "0.05" + }, + { + "maturity": "5.49486652977", + "yield": "0.05" + }, + { + "maturity": "6.49418206708", + "yield": "0.05" + }, + { + "maturity": "7.49418206708", + "yield": "0.05" + }, + { + "maturity": "8.49418206708", + "yield": "0.05" + }, + { + "maturity": "9.5", + "yield": "0.05" + } + ] + }, + + { + "name": "Euro Curve", + "points": [ + { + "maturity": "0.49828884326", + "yield": "0.05" + }, + { + "maturity": "1.49760438056", + "yield": "0.05" + }, + { + "maturity": "2.49691991786", + "yield": "0.06" + }, + { + "maturity": "3.49623545517", + "yield": "0.065" + }, + { + "maturity": "4.49555099247", + "yield": "0.065" + }, + { + "maturity": "5.49486652977", + "yield": "0.065" + }, + { + "maturity": "6.49418206708", + "yield": "0.07" + }, + { + "maturity": "7.49349760438", + "yield": "0.07" + }, + { + "maturity": "8.49281314168", + "yield": "0.08" + }, + { + "maturity": "9.5", + "yield": "0.08" + } + ] + }, + + { + "name": "Empty Curve", + "points": [ + ] + } + + ] +}