Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-23 21:48:54 +04:00
parent 5b65e1957c
commit 0c996b3bed
3 changed files with 240 additions and 0 deletions

View file

@ -0,0 +1,17 @@
package com.tangem.domain.models.quote
import com.tangem.domain.models.StatusSource
import java.math.BigDecimal
/**
* Represents the price change of a cryptocurrency asset over a specific time period.
*
* @param value The amount of price change.
* @param source The source of the price change information.
*
[REDACTED_AUTHOR]
*/
data class PriceChange(
val value: BigDecimal,
val source: StatusSource,
)

View file

@ -0,0 +1,68 @@
package com.tangem.domain.tokens.operations
import arrow.core.toNonEmptyListOrNull
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.utils.lceContent
import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.quote.PriceChange
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.utils.extensions.isZero
import com.tangem.utils.extensions.orZero
import java.math.BigDecimal
/**
* Utility object for calculating the [PriceChange] of a cryptocurrency portfolio.
*
* This object provides methods to calculate the weighted average price change of a list of cryptocurrency statuses
* based on their fiat balance weights.
*/
object PriceChangeCalculator {
/**
* Calculates the weighted average price change of a list of cryptocurrency statuses.
*
* @param statuses A list of `CryptoCurrencyStatus` objects representing the statuses of cryptocurrencies.
* @return An `Lce` object containing the calculated `PriceChange` or an error/loading state.
* - Returns `Lce.Content` with the calculated `PriceChange` if successful.
* - Returns `Lce.Loading` if the total fiat balance is still loading.
* - Returns `Lce.Error` if the total fiat balance calculation fails.
*/
fun calculate(statuses: List<CryptoCurrencyStatus>): Lce<Unit, PriceChange> {
val walletTotalFiatBalance = statuses.toNonEmptyListOrNull()
?.let(TotalFiatBalanceCalculator::calculate)
?: TokenList.Empty.totalFiatBalance
val balance = when (walletTotalFiatBalance) {
is TotalFiatBalance.Loaded -> walletTotalFiatBalance.amount
TotalFiatBalance.Loading -> {
return lceLoading()
}
TotalFiatBalance.Failed -> {
return Lce.Error(error = Unit)
}
}
if (balance.isZero()) {
return createZeroPriceChange(source = walletTotalFiatBalance.source).lceContent()
}
val total = statuses.sumOf {
val weight = it.value.fiatAmount.orZero().divide(balance)
val priceChange = it.value.priceChange.orZero()
weight * priceChange
}
return PriceChange(value = total, source = walletTotalFiatBalance.source).lceContent()
}
private fun createZeroPriceChange(source: StatusSource): PriceChange {
return PriceChange(
value = BigDecimal.ZERO.movePointLeft(2),
source = source,
)
}
}

View file

@ -0,0 +1,155 @@
package com.tangem.domain.tokens.operations
import arrow.core.nonEmptyListOf
import com.google.common.truth.Truth
import com.tangem.domain.core.utils.lceContent
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.NetworkAddress
import com.tangem.domain.models.quote.PriceChange
import com.tangem.domain.tokens.mock.MockTokens
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class PriceChangeCalculatorTest {
@Test
fun `calculate if statuses is empty list`() {
// Act
val actual = PriceChangeCalculator.calculate(statuses = emptyList())
// Assert
val expected = PriceChange(
value = BigDecimal("0.00"),
source = StatusSource.ACTUAL,
).lceContent()
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `calculate if any status is Loading`() {
// Arrange
val statuses = nonEmptyListOf(
create(
amount = BigDecimal.ZERO,
priceChange = BigDecimal("0.06"),
),
CryptoCurrencyStatus(
currency = MockTokens.token1,
value = CryptoCurrencyStatus.Loading,
),
)
// Act
val actual = PriceChangeCalculator.calculate(statuses)
// Assert
val expected = lceLoading<PriceChange>()
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `calculate if any status is Unreachable`() {
// Arrange
val statuses = nonEmptyListOf(
create(
amount = BigDecimal.ZERO,
priceChange = BigDecimal("0.06"),
),
CryptoCurrencyStatus(
currency = MockTokens.token1,
value = CryptoCurrencyStatus.Unreachable(priceChange = null, fiatRate = null, networkAddress = null),
),
)
// Act
val actual = PriceChangeCalculator.calculate(statuses)
// Assert
val expected = Unit.lceError()
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `calculate if total fiat balance is zero`() {
// Arrange
val statuses = nonEmptyListOf(
create(
amount = BigDecimal.ZERO,
priceChange = BigDecimal("0.06"),
),
create(
amount = BigDecimal("0.00"),
priceChange = BigDecimal("0.00"),
),
)
// Act
val actual = PriceChangeCalculator.calculate(statuses)
// Assert
val expected = PriceChange(
value = BigDecimal("0.00"),
source = StatusSource.ACTUAL,
).lceContent()
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `calculate if all statuses are loaded`() {
// Arrange
val statuses = nonEmptyListOf(
create(
amount = BigDecimal("5"), // 50%
priceChange = BigDecimal("0.06"), // 6%
),
create(
amount = BigDecimal("3"), // 30%
priceChange = BigDecimal("-0.02"), // -2%
),
create(
amount = BigDecimal("2"), // 20%
priceChange = BigDecimal("0.04"), // 4%
),
)
// Act
val actual = PriceChangeCalculator.calculate(statuses)
// Assert
val expected = PriceChange(
value = BigDecimal("0.032"), // 3.2%
source = StatusSource.ACTUAL,
).lceContent()
Truth.assertThat(actual).isEqualTo(expected)
}
private fun create(amount: BigDecimal, priceChange: BigDecimal): CryptoCurrencyStatus {
val value = CryptoCurrencyStatus.Loaded(
amount = amount,
fiatAmount = amount,
fiatRate = BigDecimal.ONE,
priceChange = priceChange,
yieldBalance = null,
yieldSupplyStatus = null,
hasCurrentNetworkTransactions = false,
pendingTransactions = emptySet(),
networkAddress = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(
value = "0x123",
type = NetworkAddress.Address.Type.Primary,
),
),
sources = CryptoCurrencyStatus.Sources(),
)
return CryptoCurrencyStatus(currency = MockTokens.token1, value = value)
}
}