Updated on 2026-08-14
This commit is contained in:
parent
5b65e1957c
commit
0c996b3bed
3 changed files with 240 additions and 0 deletions
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue