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": [
+ ]
+ }
+
+ ]
+}