Skip to content

Commit

Permalink
fix price per hash calculation, refactor and add better tests
Browse files Browse the repository at this point in the history
  • Loading branch information
nullpointer0x00 committed Oct 1, 2024
1 parent d00ce02 commit f939fa6
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 84 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,29 @@ 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
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
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
Expand Down Expand Up @@ -250,6 +254,26 @@ class TokenService(
return@runBlocking allPrices
}

fun processLatestTokenData(list: List<HistoricalPrice>, 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()
Expand Down Expand Up @@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)
}
}
Expand All @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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`() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
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
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 {
Expand All @@ -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<HistoricalPrice> = 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<NavEvent> = flowApiPriceFetcher.getMarkerNavByPriceDenoms(fromDate, limit)
Expand All @@ -38,19 +54,30 @@ 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")
}

assert(result.isNotEmpty()) { "Expected non-empty NavEvent list" }
}

@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")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<OsmosisHistoricalPrice> = 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
Expand Down

0 comments on commit f939fa6

Please sign in to comment.