Merge pull request #72 from corda/clint-simmdemointegrationtest

Add SIMM valuation demo integration test
This commit is contained in:
Clinton 2017-01-04 15:32:44 +00:00 committed by GitHub
commit d9663f1698
8 changed files with 306 additions and 214 deletions

View File

@ -25,49 +25,53 @@ import java.time.LocalDateTime
* the java.time API, some core types, and Kotlin data classes.
*/
object JsonSupport {
fun createDefaultMapper(identities: IdentityService): ObjectMapper {
val mapper = ServiceHubObjectMapper(identities)
mapper.enable(SerializationFeature.INDENT_OUTPUT)
mapper.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
mapper.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS)
val timeModule = SimpleModule("java.time")
timeModule.addSerializer(LocalDate::class.java, ToStringSerializer)
timeModule.addDeserializer(LocalDate::class.java, LocalDateDeserializer)
timeModule.addKeyDeserializer(LocalDate::class.java, LocalDateKeyDeserializer)
timeModule.addSerializer(LocalDateTime::class.java, ToStringSerializer)
val cordaModule = SimpleModule("core")
cordaModule.addSerializer(Party::class.java, PartySerializer)
cordaModule.addDeserializer(Party::class.java, PartyDeserializer)
cordaModule.addSerializer(BigDecimal::class.java, ToStringSerializer)
cordaModule.addDeserializer(BigDecimal::class.java, NumberDeserializers.BigDecimalDeserializer())
cordaModule.addSerializer(SecureHash::class.java, SecureHashSerializer)
// It's slightly remarkable, but apparently Jackson works out that this is the only possibility
// for a SecureHash at the moment and tries to use SHA256 directly even though we only give it SecureHash
cordaModule.addDeserializer(SecureHash.SHA256::class.java, SecureHashDeserializer())
cordaModule.addDeserializer(BusinessCalendar::class.java, CalendarDeserializer)
// For ed25519 pubkeys
cordaModule.addSerializer(EdDSAPublicKey::class.java, PublicKeySerializer)
cordaModule.addDeserializer(EdDSAPublicKey::class.java, PublicKeyDeserializer)
// For composite keys
cordaModule.addSerializer(CompositeKey::class.java, CompositeKeySerializer)
cordaModule.addDeserializer(CompositeKey::class.java, CompositeKeyDeserializer)
// For NodeInfo
// TODO this tunnels the Kryo representation as a Base58 encoded string. Replace when RPC supports this.
cordaModule.addSerializer(NodeInfo::class.java, NodeInfoSerializer)
cordaModule.addDeserializer(NodeInfo::class.java, NodeInfoDeserializer)
mapper.registerModule(timeModule)
mapper.registerModule(cordaModule)
mapper.registerModule(KotlinModule())
return mapper
val javaTimeModule : Module by lazy {
SimpleModule("java.time").apply {
addSerializer(LocalDate::class.java, ToStringSerializer)
addDeserializer(LocalDate::class.java, LocalDateDeserializer)
addKeyDeserializer(LocalDate::class.java, LocalDateKeyDeserializer)
addSerializer(LocalDateTime::class.java, ToStringSerializer)
}
}
val cordaModule : Module by lazy {
SimpleModule("core").apply {
addSerializer(Party::class.java, PartySerializer)
addDeserializer(Party::class.java, PartyDeserializer)
addSerializer(BigDecimal::class.java, ToStringSerializer)
addDeserializer(BigDecimal::class.java, NumberDeserializers.BigDecimalDeserializer())
addSerializer(SecureHash::class.java, SecureHashSerializer)
// It's slightly remarkable, but apparently Jackson works out that this is the only possibility
// for a SecureHash at the moment and tries to use SHA256 directly even though we only give it SecureHash
addDeserializer(SecureHash.SHA256::class.java, SecureHashDeserializer())
addDeserializer(BusinessCalendar::class.java, CalendarDeserializer)
// For ed25519 pubkeys
addSerializer(EdDSAPublicKey::class.java, PublicKeySerializer)
addDeserializer(EdDSAPublicKey::class.java, PublicKeyDeserializer)
// For composite keys
addSerializer(CompositeKey::class.java, CompositeKeySerializer)
addDeserializer(CompositeKey::class.java, CompositeKeyDeserializer)
// For NodeInfo
// TODO this tunnels the Kryo representation as a Base58 encoded string. Replace when RPC supports this.
addSerializer(NodeInfo::class.java, NodeInfoSerializer)
addDeserializer(NodeInfo::class.java, NodeInfoDeserializer)
}
}
fun createDefaultMapper(identities: IdentityService): ObjectMapper =
ServiceHubObjectMapper(identities).apply {
enable(SerializationFeature.INDENT_OUTPUT)
enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS)
registerModule(javaTimeModule)
registerModule(cordaModule)
registerModule(KotlinModule())
}
class ServiceHubObjectMapper(val identities: IdentityService) : ObjectMapper()
object ToStringSerializer : JsonSerializer<Any>() {

View File

@ -34,6 +34,18 @@ sourceSets {
srcDir "../../config/test"
}
}
integrationTest {
kotlin {
compileClasspath += main.output + test.output
runtimeClasspath += main.output + test.output
srcDir file('src/integration-test/kotlin')
}
}
}
configurations {
integrationTestCompile.extendsFrom testCompile
integrationTestRuntime.extendsFrom testRuntime
}
dependencies {
@ -105,6 +117,10 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['build']) {
}
}
task integrationTest(type: Test, dependsOn: []) {
testClassesDir = sourceSets.integrationTest.output.classesDir
classpath = sourceSets.integrationTest.runtimeClasspath
}
task npmInstall(type: Exec) {
workingDir 'src/main/web'

View File

@ -0,0 +1,78 @@
package net.corda.vega
import com.opengamma.strata.product.common.BuySell
import net.corda.core.getOrThrow
import net.corda.core.node.services.ServiceInfo
import net.corda.node.driver.NodeHandle
import net.corda.node.driver.driver
import net.corda.node.services.transactions.SimpleNotaryService
import net.corda.testing.IntegrationTestCategory
import net.corda.testing.getHostAndPort
import net.corda.testing.http.HttpApi
import net.corda.vega.api.PortfolioApi
import net.corda.vega.api.PortfolioApiUtils
import net.corda.vega.api.SwapDataModel
import net.corda.vega.api.SwapDataView
import net.corda.vega.portfolio.Portfolio
import org.junit.Test
import java.math.BigDecimal
import java.time.LocalDate
import java.util.*
import java.util.concurrent.Future
class SimmValuationTest: IntegrationTestCategory {
private companion object {
// SIMM demo can only currently handle one valuation date due to a lack of market data or a market data source.
val valuationDate = LocalDate.parse("2016-06-06")
val nodeALegalName = "Bank A"
val nodeBLegalName = "Bank B"
val testTradeId = "trade1"
}
@Test fun `runs SIMM valuation demo`() {
driver(isDebug = true) {
startNode("Controller", setOf(ServiceInfo(SimpleNotaryService.type))).getOrThrow()
val nodeA = getSimmNodeApi(startNode(nodeALegalName))
val nodeB = getSimmNodeApi(startNode(nodeBLegalName))
val nodeBParty = getPartyWithName(nodeA, nodeBLegalName)
val nodeAParty = getPartyWithName(nodeB, nodeALegalName)
assert(createTradeBetween(nodeA, nodeBParty, testTradeId))
assert(tradeExists(nodeB, nodeAParty, testTradeId))
assert(runValuationsBetween(nodeA, nodeBParty))
assert(valuationExists(nodeB, nodeAParty))
}
}
private fun getSimmNodeApi(futureNode: Future<NodeHandle>): HttpApi {
val nodeAddr = futureNode.getOrThrow().config.getHostAndPort("webAddress")
return HttpApi.fromHostAndPort(nodeAddr, "api/simmvaluationdemo")
}
private fun getPartyWithName(partyApi: HttpApi, countryparty: String): PortfolioApi.ApiParty =
getAvailablePartiesFor(partyApi).counterparties.single { it.text == countryparty }
private fun getAvailablePartiesFor(partyApi: HttpApi): PortfolioApi.AvailableParties {
return partyApi.getJson<PortfolioApi.AvailableParties>("whoami")
}
private fun createTradeBetween(partyApi: HttpApi, counterparty: PortfolioApi.ApiParty, tradeId: String): Boolean {
val trade = SwapDataModel(tradeId, "desc", valuationDate, "EUR_FIXED_1Y_EURIBOR_3M",
valuationDate, LocalDate.parse("2020-01-02"), BuySell.BUY, BigDecimal.valueOf(1000), BigDecimal.valueOf(0.1))
return partyApi.putJson("${counterparty.id}/trades", trade)
}
private fun tradeExists(partyApi: HttpApi, counterparty: PortfolioApi.ApiParty, tradeId: String): Boolean {
val trades = partyApi.getJson<Array<SwapDataView>>("${counterparty.id}/trades")
return (trades.find { it.id == tradeId } != null)
}
private fun runValuationsBetween(partyApi: HttpApi, counterparty: PortfolioApi.ApiParty): Boolean {
return partyApi.postJson("${counterparty.id}/portfolio/valuations/calculate", PortfolioApi.ValuationCreationParams(valuationDate))
}
private fun valuationExists(partyApi: HttpApi, counterparty: PortfolioApi.ApiParty): Boolean {
val valuations = partyApi.getJson<PortfolioApiUtils.ValuationsView>("${counterparty.id}/portfolio/valuations")
return (valuations.initialMargin.call["total"] != 0.0)
}
}

View File

@ -1,20 +0,0 @@
package net.corda.vega.api
/**
* A small JSON DSL to create structures for APIs on the fly that mimic JSON in structure.
* Use: json { obj("a" to 100, "b" to "hello", "c" to arr(1, 2, "c")) }
*/
class JsonBuilder {
fun obj(vararg objs: Pair<String, Any>): Map<String, Any> {
return objs.toMap()
}
fun arr(vararg objs: Any): List<Any> {
return objs.toList()
}
}
fun json(body: JsonBuilder.() -> Map<String, Any>): Map<String, Any> {
val jsonWrapper = JsonBuilder()
return jsonWrapper.body()
}

View File

@ -98,11 +98,9 @@ class PortfolioApi(val rpc: CordaRPCOps) {
@Path("business-date")
@Produces(MediaType.APPLICATION_JSON)
fun getBusinessDate(): Any {
return json {
obj(
"business-date" to LocalDateTime.ofInstant(rpc.currentNodeTime(), ZoneId.systemDefault()).toLocalDate()
)
}
return mapOf(
"business-date" to LocalDateTime.ofInstant(rpc.currentNodeTime(), ZoneId.systemDefault()).toLocalDate()
)
}
/**
@ -195,12 +193,10 @@ class PortfolioApi(val rpc: CordaRPCOps) {
return withParty(partyName) { party ->
val trades = getTradesWith(party)
val portfolio = Portfolio(trades)
val summary = json {
obj(
"trades" to portfolio.trades.size,
"notional" to portfolio.getNotionalForParty(ownParty).toDouble()
)
}
val summary = mapOf(
"trades" to portfolio.trades.size,
"notional" to portfolio.getNotionalForParty(ownParty).toDouble()
)
Response.ok().entity(summary).build()
}
}
@ -239,28 +235,23 @@ class PortfolioApi(val rpc: CordaRPCOps) {
}
}
data class ApiParty(val id: String, val text: String)
data class AvailableParties(val self: ApiParty, val counterparties: List<ApiParty>)
/**
* Returns the identity of the current node as well as a list of other counterparties that it is aware of.
*/
@GET
@Path("whoami")
@Produces(MediaType.APPLICATION_JSON)
fun getWhoAmI(): Any {
val counterParties = rpc.networkMapUpdates().first.filter { it.legalIdentity.name != "NetworkMapService" && it.legalIdentity.name != "Notary" && it.legalIdentity.name != ownParty.name }
return json {
obj(
"self" to obj(
"id" to ownParty.owningKey.toBase58String(),
"text" to ownParty.name
),
"counterparties" to counterParties.map {
obj(
"id" to it.legalIdentity.owningKey.toBase58String(),
"text" to it.legalIdentity.name
)
}
)
fun getWhoAmI(): AvailableParties {
val counterParties = rpc.networkMapUpdates().first.filter {
it.legalIdentity.name != "NetworkMapService" && it.legalIdentity.name != "Notary" && it.legalIdentity.name != ownParty.name
}
return AvailableParties(
self = ApiParty(ownParty.owningKey.toBase58String(), ownParty.name),
counterparties = counterParties.map { ApiParty(it.legalIdentity.owningKey.toBase58String(), it.legalIdentity.name) })
}
data class ValuationCreationParams(val valuationDate: LocalDate)

View File

@ -15,7 +15,16 @@ import java.time.LocalDate
* API JSON generation functions for larger JSON outputs.
*/
class PortfolioApiUtils(private val ownParty: Party) {
fun createValuations(state: PortfolioState, portfolio: Portfolio): Any {
data class InitialMarginView(val baseCurrency: String, val post: Map<String, Double>, val call: Map<String, Double>, val agreed: Boolean)
data class ValuationsView(
val businessDate: LocalDate,
val portfolio: Map<String, Any>,
val marketData: Map<String, Any>,
val sensitivities: Map<String, Any>,
val initialMargin: InitialMarginView,
val confirmation: Map<String, Any>)
fun createValuations(state: PortfolioState, portfolio: Portfolio): ValuationsView {
val valuation = state.valuation!!
val currency = if (portfolio.trades.isNotEmpty()) {
@ -32,141 +41,137 @@ class PortfolioApiUtils(private val ownParty: Party) {
val completeSubgroups = subgroups.mapValues { it.value.mapValues { it.value[0].third.toDouble() }.toSortedMap() }
val yieldCurves = json {
obj(
"name" to "EUR",
"values" to completeSubgroups.get("EUR")!!.filter { !it.key.contains("Fixing") }.map {
json {
obj(
"tenor" to it.key,
"rate" to it.value
)
}
}
)
}
val fixings = json {
obj(
"name" to "EUR",
"values" to completeSubgroups.get("EUR")!!.filter { it.key.contains("Fixing") }.map {
json {
obj(
"tenor" to it.key,
"rate" to it.value
)
}
}
)
}
val yieldCurves = mapOf(
"name" to "EUR",
"values" to completeSubgroups.get("EUR")!!.filter { !it.key.contains("Fixing") }.map {
mapOf(
"tenor" to it.key,
"rate" to it.value
)
}
)
val fixings = mapOf(
"name" to "EUR",
"values" to completeSubgroups.get("EUR")!!.filter { it.key.contains("Fixing") }.map {
mapOf(
"tenor" to it.key,
"rate" to it.value
)
}
)
val processedSensitivities = valuation.totalSensivities.sensitivities.map { it.marketDataName to it.parameterMetadata.map { it.label }.zip(it.sensitivity.toList()).toMap() }.toMap()
return json {
obj(
"businessDate" to LocalDate.now(),
"portfolio" to obj(
"trades" to tradeCount,
"baseCurrency" to currency,
"IRFX" to tradeCount,
"commodity" to 0,
"equity" to 0,
"credit" to 0,
"total" to tradeCount,
"agreed" to true
),
"marketData" to obj(
"yieldCurves" to yieldCurves,
"fixings" to fixings,
"agreed" to true
),
"sensitivities" to obj("curves" to processedSensitivities,
"currency" to valuation.currencySensitivies.amounts.toList().map {
obj(
"currency" to it.currency.code,
"amount" to it.amount
)
},
"agreed" to true
),
"initialMargin" to obj(
"baseCurrency" to currency,
"post" to obj(
"IRFX" to valuation.margin.first,
"commodity" to 0,
"equity" to 0,
"credit" to 0,
"total" to valuation.margin.first
),
"call" to obj(
"IRFX" to valuation.margin.first,
"commodity" to 0,
"equity" to 0,
"credit" to 0,
"total" to valuation.margin.first
),
"agreed" to true
),
"confirmation" to obj(
"hash" to state.hash().toString(),
"agreed" to true
)
)
}
val initialMarginView = InitialMarginView(
baseCurrency = currency,
post = mapOf(
"IRFX" to valuation.margin.first,
"commodity" to 0.0,
"equity" to 0.0,
"credit" to 0.0,
"total" to valuation.margin.first
),
call = mapOf(
"IRFX" to valuation.margin.first,
"commodity" to 0.0,
"equity" to 0.0,
"credit" to 0.0,
"total" to valuation.margin.first
),
agreed = true)
return ValuationsView(
businessDate = LocalDate.now(),
portfolio = mapOf(
"trades" to tradeCount,
"baseCurrency" to currency,
"IRFX" to tradeCount,
"commodity" to 0,
"equity" to 0,
"credit" to 0,
"total" to tradeCount,
"agreed" to true
),
marketData = mapOf(
"yieldCurves" to yieldCurves,
"fixings" to fixings,
"agreed" to true
),
sensitivities = mapOf("curves" to processedSensitivities,
"currency" to valuation.currencySensitivies.amounts.toList().map {
mapOf(
"currency" to it.currency.code,
"amount" to it.amount
)
},
"agreed" to true
),
initialMargin = initialMarginView,
confirmation = mapOf(
"hash" to state.hash().toString(),
"agreed" to true
)
)
}
fun createTradeView(state: IRSState): Any {
data class TradeView(
val fixedLeg: Map<String, Any>,
val floatingLeg: Map<String, Any>,
val common: Map<String, Any>,
val ref: String)
fun createTradeView(state: IRSState): TradeView {
val trade = if (state.buyer.name == ownParty.name) state.swap.toFloatingLeg() else state.swap.toFloatingLeg()
val fixedLeg = trade.product.legs.first { it.type == SwapLegType.FIXED } as RateCalculationSwapLeg
val floatingLeg = trade.product.legs.first { it.type != SwapLegType.FIXED } as RateCalculationSwapLeg
val fixedRate = fixedLeg.calculation as FixedRateCalculation
val floatingRate = floatingLeg.calculation as IborRateCalculation
return json {
obj(
"fixedLeg" to obj(
"fixedRatePayer" to state.buyer.name,
"notional" to obj(
"token" to fixedLeg.currency.code,
"quantity" to fixedLeg.notionalSchedule.amount.initialValue
),
"paymentFrequency" to fixedLeg.paymentSchedule.paymentFrequency.toString(),
"effectiveDate" to fixedLeg.startDate.unadjusted,
"terminationDate" to fixedLeg.endDate.unadjusted,
"fixedRate" to obj(
"value" to fixedRate.rate.initialValue
),
"paymentRule" to fixedLeg.paymentSchedule.paymentRelativeTo.name,
"calendar" to arr("TODO"),
"paymentCalendar" to obj() // TODO
),
"floatingLeg" to obj(
"floatingRatePayer" to state.seller.name,
"notional" to obj(
"token" to floatingLeg.currency.code,
"quantity" to floatingLeg.notionalSchedule.amount.initialValue
),
"paymentFrequency" to floatingLeg.paymentSchedule.paymentFrequency.toString(),
"effectiveDate" to floatingLeg.startDate.unadjusted,
"terminationDate" to floatingLeg.endDate.unadjusted,
"index" to floatingRate.index.name,
"paymentRule" to floatingLeg.paymentSchedule.paymentRelativeTo,
"calendar" to arr("TODO"),
"paymentCalendar" to arr("TODO"),
"fixingCalendar" to obj() // TODO
),
"common" to obj(
"valuationDate" to trade.product.startDate.unadjusted,
"hashLegalDocs" to state.contract.legalContractReference.toString(),
"interestRate" to obj(
"name" to "TODO",
"oracle" to "TODO",
"tenor" to obj(
"name" to "TODO"
)
)
),
"ref" to trade.info.id.get().value
)
}
return TradeView(
fixedLeg = mapOf(
"fixedRatePayer" to state.buyer.name,
"notional" to mapOf(
"token" to fixedLeg.currency.code,
"quantity" to fixedLeg.notionalSchedule.amount.initialValue
),
"paymentFrequency" to fixedLeg.paymentSchedule.paymentFrequency.toString(),
"effectiveDate" to fixedLeg.startDate.unadjusted,
"terminationDate" to fixedLeg.endDate.unadjusted,
"fixedRate" to mapOf(
"value" to fixedRate.rate.initialValue
),
"paymentRule" to fixedLeg.paymentSchedule.paymentRelativeTo.name,
"calendar" to listOf("TODO"),
"paymentCalendar" to mapOf<String, Any>() // TODO
),
floatingLeg = mapOf(
"floatingRatePayer" to state.seller.name,
"notional" to mapOf(
"token" to floatingLeg.currency.code,
"quantity" to floatingLeg.notionalSchedule.amount.initialValue
),
"paymentFrequency" to floatingLeg.paymentSchedule.paymentFrequency.toString(),
"effectiveDate" to floatingLeg.startDate.unadjusted,
"terminationDate" to floatingLeg.endDate.unadjusted,
"index" to floatingRate.index.name,
"paymentRule" to floatingLeg.paymentSchedule.paymentRelativeTo,
"calendar" to listOf("TODO"),
"paymentCalendar" to listOf("TODO"),
"fixingCalendar" to mapOf<String, Any>() // TODO
),
common = mapOf(
"valuationDate" to trade.product.startDate.unadjusted,
"hashLegalDocs" to state.contract.legalContractReference.toString(),
"interestRate" to mapOf(
"name" to "TODO",
"oracle" to "TODO",
"tenor" to mapOf(
"name" to "TODO"
)
)
),
ref = trade.info.id.get().value
)
}
}

View File

@ -18,7 +18,12 @@ class HttpApi(val root: URL) {
*/
fun postJson(path: String, data: Any = Unit) = HttpUtils.postJson(URL(root, path), toJson(data))
private fun toJson(any: Any) = if (any is String) any else ObjectMapper().writeValueAsString(any)
/**
* Send a GET request to the path on the API specified.
*/
inline fun<reified T: Any> getJson(path: String, params: Map<String, String> = mapOf()) = HttpUtils.getJson<T>(URL(root, path), params)
private fun toJson(any: Any) = any as? String ?: HttpUtils.defaultMapper.writeValueAsString(any)
companion object {
fun fromHostAndPort(hostAndPort: HostAndPort, base: String, protocol: String = "http"): HttpApi

View File

@ -1,6 +1,9 @@
package net.corda.testing.http
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
import net.corda.core.utilities.loggerFor
import net.corda.node.utilities.JsonSupport
import okhttp3.*
import java.net.URL
import java.util.concurrent.TimeUnit
@ -15,6 +18,9 @@ object HttpUtils {
.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS).build()
}
val defaultMapper: ObjectMapper by lazy {
ObjectMapper().registerModule(JsonSupport.javaTimeModule).registerModule(KotlinModule())
}
fun putJson(url: URL, data: String) : Boolean {
val body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), data)
@ -26,12 +32,19 @@ object HttpUtils {
return makeRequest(Request.Builder().url(url).header("Content-Type", "application/json").post(body).build())
}
inline fun<reified T: Any> getJson(url: URL, params: Map<String, String> = mapOf()) : T {
val paramString = if(params.isEmpty()) "" else "?" + params.map { "${it.key}=${it.value}" }.joinToString("&")
val parameterisedUrl = URL(url.toExternalForm() + paramString)
return defaultMapper.readValue(parameterisedUrl, T::class.java)
}
private fun makeRequest(request: Request): Boolean {
val response = client.newCall(request).execute()
if (!response.isSuccessful) {
logger.error("Could not fulfill HTTP request of type ${request.method()} to ${request.url()}. Status Code: ${response.code()}. Message: ${response.body().string()}")
}
return response.isSuccessful
}
}