diff --git a/service/src/main/kotlin/io/provenance/explorer/service/TokenService.kt b/service/src/main/kotlin/io/provenance/explorer/service/TokenService.kt index f4361d8a..720ab407 100644 --- a/service/src/main/kotlin/io/provenance/explorer/service/TokenService.kt +++ b/service/src/main/kotlin/io/provenance/explorer/service/TokenService.kt @@ -20,11 +20,13 @@ import io.provenance.explorer.domain.entities.vestingAccountTypes import io.provenance.explorer.domain.exceptions.validate import io.provenance.explorer.domain.extensions.CsvData import io.provenance.explorer.domain.extensions.pageCountOfResults +import io.provenance.explorer.domain.extensions.percentChange import io.provenance.explorer.domain.extensions.roundWhole import io.provenance.explorer.domain.extensions.startOfDay import io.provenance.explorer.domain.extensions.toCoinStr import io.provenance.explorer.domain.extensions.toOffset import io.provenance.explorer.domain.extensions.toPercentage +import io.provenance.explorer.domain.extensions.toThirdDecimal import io.provenance.explorer.domain.models.HistoricalPrice import io.provenance.explorer.domain.models.explorer.TokenHistoricalDataRequest import io.provenance.explorer.domain.models.toCsv @@ -32,6 +34,7 @@ import io.provenance.explorer.grpc.flow.FlowApiGrpcClient import io.provenance.explorer.grpc.v1.AccountGrpcClient import io.provenance.explorer.model.AssetHolder import io.provenance.explorer.model.CmcLatestDataAbbrev +import io.provenance.explorer.model.CmcLatestQuoteAbbrev import io.provenance.explorer.model.RichAccount import io.provenance.explorer.model.TokenDistribution import io.provenance.explorer.model.TokenDistributionAmount @@ -39,6 +42,7 @@ import io.provenance.explorer.model.TokenSupply import io.provenance.explorer.model.base.CoinStr import io.provenance.explorer.model.base.CountStrTotal import io.provenance.explorer.model.base.PagedResults +import io.provenance.explorer.model.base.USD_UPPER import io.provenance.explorer.service.pricing.fetchers.HistoricalPriceFetcher import io.provenance.explorer.service.pricing.fetchers.HistoricalPriceFetcherFactory import kotlinx.coroutines.flow.asFlow @@ -250,6 +254,26 @@ class TokenService( return@runBlocking allPrices } + fun processLatestTokenData(list: List, today: DateTime): CmcLatestDataAbbrev? { + val prevRecord = list.firstOrNull() ?: return null + val price = list.last().close.toThirdDecimal() + val percentChg = price.percentChange(prevRecord.close.toThirdDecimal()) + val vol24Hr = list.sumOf { it.volume.toThirdDecimal() }.stripTrailingZeros() + val marketCap = price.multiply(totalSupply().divide(UTILITY_TOKEN_BASE_MULTIPLIER)).toThirdDecimal() + + return CmcLatestDataAbbrev( + today, + mapOf(USD_UPPER to CmcLatestQuoteAbbrev(price, percentChg, vol24Hr, marketCap, today)) + ) + } + + fun cacheLatestTokenData(data: CmcLatestDataAbbrev) { + CacheUpdateRecord.updateCacheByKey( + CacheKeys.UTILITY_TOKEN_LATEST.key, + VANILLA_MAPPER.writeValueAsString(data) + ) + } + fun getHashPricingDataDownload(filters: TokenHistoricalDataRequest, resp: ServletOutputStream): ZipOutputStream { validate(filters.datesValidation()) val baseFileName = filters.getFileNameBase() @@ -279,29 +303,4 @@ class TokenService( } } -/** - * Calculates the price per hash unit based on the total price in USD (expressed as whole numbers - * where 12345 equals $12.345 USD) and the volume in nHash (nano Hash). - * - * @param priceAmount The total price in whole-number USD cents (e.g., 12345 equals $12.345 USD). - * @param volumeNhash The volume of the transaction in nHash (nano Hash). - * 1 Hash = 1,000,000,000 nHash. - * @return The price per hash unit. Returns 0.0 if the volumeNhash is 0 to avoid division by zero. - */ -fun calculatePricePerHash(priceAmountMillis: Long, volumeNhash: Long): Double { - val volumeHash = calculateVolumeHash(volumeNhash) - if (volumeHash == BigDecimal.ZERO) { - return 0.0 - } - val pricePerHash = BigDecimal(priceAmountMillis).divide(volumeHash, 10, RoundingMode.HALF_UP) - return pricePerHash.divide(BigDecimal(1000), 10, RoundingMode.HALF_UP).toDouble() -} - -fun calculateVolumeHash(volumeNhash: Long): BigDecimal { - if (volumeNhash == 0L) { - return BigDecimal.ZERO - } - return BigDecimal(volumeNhash).divide(UTILITY_TOKEN_BASE_MULTIPLIER, 10, RoundingMode.HALF_UP) -} - fun BigDecimal.asPercentOf(divisor: BigDecimal): BigDecimal = this.divide(divisor, 20, RoundingMode.CEILING) diff --git a/service/src/main/kotlin/io/provenance/explorer/service/async/ScheduledTaskService.kt b/service/src/main/kotlin/io/provenance/explorer/service/async/ScheduledTaskService.kt index 04866a6a..0ece1310 100644 --- a/service/src/main/kotlin/io/provenance/explorer/service/async/ScheduledTaskService.kt +++ b/service/src/main/kotlin/io/provenance/explorer/service/async/ScheduledTaskService.kt @@ -360,23 +360,14 @@ class ScheduledTaskService( fun updateTokenLatest() { val today = DateTime.now().withZone(DateTimeZone.UTC) val startDate = today.minusDays(1) - tokenService.fetchHistoricalPriceData(startDate) - ?.sortedBy { it.time } - ?.let { list -> - val prevRecord = list.firstOrNull() ?: return - val price = list.last().close.toThirdDecimal() - val percentChg = price.percentChange(prevRecord.close.toThirdDecimal()) - val vol24Hr = list.sumOf { it.volume.toThirdDecimal() }.stripTrailingZeros() - val marketCap = price.multiply(tokenService.totalSupply().divide(UTILITY_TOKEN_BASE_MULTIPLIER)).toThirdDecimal() - val rec = CmcLatestDataAbbrev( - today, - mapOf(USD_UPPER to CmcLatestQuoteAbbrev(price, percentChg, vol24Hr, marketCap, today)) - ) - CacheUpdateRecord.updateCacheByKey( - CacheKeys.UTILITY_TOKEN_LATEST.key, - VANILLA_MAPPER.writeValueAsString(rec) - ) + val list = tokenService.fetchHistoricalPriceData(startDate)?.sortedBy { it.time } + + list?.let { + val latestData = tokenService.processLatestTokenData(it, today) + latestData?.let { data -> + tokenService.cacheLatestTokenData(data) } + } } // Remove once the ranges have been updated diff --git a/service/src/main/kotlin/io/provenance/explorer/service/pricing/fetchers/FlowApiPriceFetcher.kt b/service/src/main/kotlin/io/provenance/explorer/service/pricing/fetchers/FlowApiPriceFetcher.kt index 8b4f72e5..c437bd77 100644 --- a/service/src/main/kotlin/io/provenance/explorer/service/pricing/fetchers/FlowApiPriceFetcher.kt +++ b/service/src/main/kotlin/io/provenance/explorer/service/pricing/fetchers/FlowApiPriceFetcher.kt @@ -18,14 +18,14 @@ class FlowApiPriceFetcher( val onChainNavEvents = getMarkerNavByPriceDenoms(fromDate, 17800) return onChainNavEvents.map { navEvent -> val volumeHash = calculateVolumeHash(navEvent.volume) - val pricePerHash = calculatePricePerHash(navEvent.priceAmount, navEvent.volume) + val pricePerHash = getPricePerHashFromMicroUsd(navEvent.priceAmount, navEvent.volume) HistoricalPrice( time = navEvent.blockTime, - high = BigDecimal(pricePerHash), - low = BigDecimal(pricePerHash), - close = BigDecimal(pricePerHash), - open = BigDecimal(pricePerHash), - volume = BigDecimal(pricePerHash).multiply(volumeHash) + high = pricePerHash, + low = pricePerHash, + close = pricePerHash, + open = pricePerHash, + volume = pricePerHash.multiply(volumeHash) ) } } @@ -42,20 +42,21 @@ class FlowApiPriceFetcher( } /** - * Calculates the price per hash unit based on the total price in USD (expressed as whole numbers - * where 12345 equals $12.345 USD) and the volume in nHash (nano Hash). + * Calculates the price per hash unit based on the total price in micro-USD and the volume in nHash. * - * @param priceAmount The total price in whole-number USD cents (e.g., 12345 equals $12.345 USD). + * @param priceAmountMicros The total price in micro-USD (e.g., 123456789 equals $123.456789 USD). * @param volumeNhash The volume of the transaction in nHash (nano Hash). * 1 Hash = 1,000,000,000 nHash. - * @return The price per hash unit. Returns 0.0 if the volumeNhash is 0 to avoid division by zero. + * @return The price per hash unit in USD, rounded down to 3 decimal places. + * Returns 0.0 if the volumeNhash is 0 to avoid division by zero. */ - fun calculatePricePerHash(priceAmountMillis: Long, volumeNhash: Long): Double { - val volumeHash = io.provenance.explorer.service.calculateVolumeHash(volumeNhash) - if (volumeHash == BigDecimal.ZERO) { - return 0.0 + fun getPricePerHashFromMicroUsd(priceAmountMicros: Long, volumeNhash: Long): BigDecimal { + if (volumeNhash == 0L) { + return BigDecimal.ZERO } - val pricePerHash = BigDecimal(priceAmountMillis).divide(volumeHash, 10, RoundingMode.HALF_UP) - return pricePerHash.divide(BigDecimal(1000), 10, RoundingMode.HALF_UP).toDouble() + val volumeHash = calculateVolumeHash(volumeNhash) + val priceInUsd = BigDecimal(priceAmountMicros).divide(BigDecimal(1_000_000), 10, RoundingMode.HALF_UP) + val pricePerHash = priceInUsd.divide(volumeHash, 10, RoundingMode.HALF_UP) + return pricePerHash.setScale(3, RoundingMode.FLOOR) } } diff --git a/service/src/test/kotlin/io/provenance/explorer/service/TokenServiceTest.kt b/service/src/test/kotlin/io/provenance/explorer/service/TokenServiceTest.kt index f4691dd3..e9e64741 100644 --- a/service/src/test/kotlin/io/provenance/explorer/service/TokenServiceTest.kt +++ b/service/src/test/kotlin/io/provenance/explorer/service/TokenServiceTest.kt @@ -55,23 +55,6 @@ class TokenServiceTest { assert(result.isNotEmpty()) { "Expected non-empty list of HistoricalPrice" } } - @Test - fun `test calculatePricePerHash`() { - val priceAmount = 12345L - val volume = 1000000000000L // 1 Hash = 1,000,000,000 nHash - - val result = calculatePricePerHash(priceAmount, volume) - assertEquals(12.345, result, "Price per hash calculation is incorrect") - } - - @Test - fun `test calculateVolumeHash`() { - val volumeNhash = 1000000000000L // 1 Hash = 1,000,000,000 nHash - - val result = calculateVolumeHash(volumeNhash) - assertEquals(1.toBigDecimal(), result, "Volume hash calculation is incorrect") - } - @Test @Disabled("Test was used to manually call the endpoint") fun `test getTokenDistributionStats`() { diff --git a/service/src/test/kotlin/io/provenance/explorer/service/pricing/fetchers/FlowApiPriceFetcherTest.kt b/service/src/test/kotlin/io/provenance/explorer/service/pricing/fetchers/FlowApiPriceFetcherTest.kt index 78644d03..bd03ebec 100644 --- a/service/src/test/kotlin/io/provenance/explorer/service/pricing/fetchers/FlowApiPriceFetcherTest.kt +++ b/service/src/test/kotlin/io/provenance/explorer/service/pricing/fetchers/FlowApiPriceFetcherTest.kt @@ -1,6 +1,7 @@ package io.provenance.explorer.service.pricing.fetchers import io.provenance.explorer.config.ExplorerProperties +import io.provenance.explorer.domain.models.HistoricalPrice import io.provenance.explorer.grpc.flow.FlowApiGrpcClient import io.provlabs.flow.api.NavEvent import org.joda.time.DateTime @@ -8,6 +9,7 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test +import java.math.BigDecimal import java.net.URI class FlowApiPriceFetcherTest { @@ -25,8 +27,22 @@ class FlowApiPriceFetcherTest { @Test @Disabled("Test was used to manually call the endpoint") - fun `test fetchOnChainNavData and print results`() { - val fromDate = DateTime.now().minusDays(7) + fun `test fetchHistoricalPrice and print results`() { + val fromDate = DateTime.now().minusDays(1) + + val result: List = flowApiPriceFetcher.fetchHistoricalPrice(fromDate) + result.forEach { + println("Time: ${DateTime(it.time * 1000)}, Open: ${it.open}, High: ${it.high}, Low: ${it.low}, Close: ${it.close}, Volume: ${it.volume}") + } + + val totalVolume = result.sumOf { it.volume } + println("Total Volume: $totalVolume") + } + + @Test + @Disabled("Test was used to manually call the endpoint") + fun `test getMarkerNavByPriceDenoms and print results`() { + val fromDate = DateTime.now().minusDays(1) val limit = 100 val result: List = flowApiPriceFetcher.getMarkerNavByPriceDenoms(fromDate, limit) @@ -38,7 +54,7 @@ class FlowApiPriceFetcherTest { } result.forEach { navEvent -> - val pricePerHash = flowApiPriceFetcher.calculatePricePerHash(navEvent.priceAmount, navEvent.volume) + val pricePerHash = flowApiPriceFetcher.getPricePerHashFromMicroUsd(navEvent.priceAmount, navEvent.volume) println("NavEvent: Time=${DateTime(navEvent.blockTime * 1000)}, PriceDenom=${navEvent.priceDenom}, Hash Price: $pricePerHash") } @@ -46,11 +62,22 @@ class FlowApiPriceFetcherTest { } @Test - fun `test calculatePricePerHash with multiple scenarios`() { - var result = flowApiPriceFetcher.calculatePricePerHash(12345L, ExplorerProperties.UTILITY_TOKEN_BASE_MULTIPLIER.toLong()) - assertEquals(12.345, result, "Price per hash calculation is incorrect") + fun `test calculatePricePerHashFromMicroUsd`() { + var result = flowApiPriceFetcher.getPricePerHashFromMicroUsd( + 4800000000L, + 300000000000000 + ) + assertEquals(BigDecimal("0.016"), result, "Price per hash calculation is incorrect") + + result = flowApiPriceFetcher.getPricePerHashFromMicroUsd(12345L, 0L) + assertEquals(BigDecimal.ZERO, result, "Should return 0.0 when volume is 0") + } + + @Test + fun `test calculateVolumeHash`() { + val volumeNhash = 1000000000000L - result = flowApiPriceFetcher.calculatePricePerHash(12345L, 0L) - assertEquals(0.0, result, "Should return 0.0 when volume is 0") + val result = flowApiPriceFetcher.calculateVolumeHash(volumeNhash) + assertEquals(1.toBigDecimal(), result, "Volume hash calculation is incorrect") } } diff --git a/service/src/test/kotlin/io/provenance/explorer/service/pricing/fetchers/OsmosisPriceFetcherTest.kt b/service/src/test/kotlin/io/provenance/explorer/service/pricing/fetchers/OsmosisPriceFetcherTest.kt index 6f13b924..7cb0bec0 100644 --- a/service/src/test/kotlin/io/provenance/explorer/service/pricing/fetchers/OsmosisPriceFetcherTest.kt +++ b/service/src/test/kotlin/io/provenance/explorer/service/pricing/fetchers/OsmosisPriceFetcherTest.kt @@ -21,13 +21,14 @@ class OsmosisPriceFetcherTest { @Test @Disabled("Test was used to manually call the endpoint") fun `test fetchOsmosisData and print results`() = runBlocking { - val fromDate = DateTime.parse("2024-05-08") + val fromDate = DateTime.parse("2024-10-01") val result: List = osmosisPriceFetcher.fetchOsmosisData(fromDate) - result.forEach { println("Time: ${DateTime(it.time * 1000)}, Open: ${it.open}, High: ${it.high}, Low: ${it.low}, Close: ${it.close}, Volume: ${it.volume}") } + val totalVolume = result.sumOf { it.volume } + println("Total Volume: $totalVolume") } @Test