InsightsService.java
package com.distasilucas.cryptobalancetracker.service;
import com.distasilucas.cryptobalancetracker.entity.Crypto;
import com.distasilucas.cryptobalancetracker.entity.DateBalance;
import com.distasilucas.cryptobalancetracker.entity.Platform;
import com.distasilucas.cryptobalancetracker.entity.UserCrypto;
import com.distasilucas.cryptobalancetracker.model.BalanceType;
import com.distasilucas.cryptobalancetracker.model.DateRange;
import com.distasilucas.cryptobalancetracker.model.SortParams;
import com.distasilucas.cryptobalancetracker.model.response.insights.BalanceChanges;
import com.distasilucas.cryptobalancetracker.model.response.insights.BalancesResponse;
import com.distasilucas.cryptobalancetracker.model.response.insights.CirculatingSupply;
import com.distasilucas.cryptobalancetracker.model.response.insights.CryptoInfo;
import com.distasilucas.cryptobalancetracker.model.response.insights.CryptoInsights;
import com.distasilucas.cryptobalancetracker.model.response.insights.DatesBalanceResponse;
import com.distasilucas.cryptobalancetracker.model.response.insights.DateBalances;
import com.distasilucas.cryptobalancetracker.model.response.insights.DifferencesChanges;
import com.distasilucas.cryptobalancetracker.model.response.insights.MarketData;
import com.distasilucas.cryptobalancetracker.model.response.insights.UserCryptosInsights;
import com.distasilucas.cryptobalancetracker.model.response.insights.crypto.CryptoInsightResponse;
import com.distasilucas.cryptobalancetracker.model.response.insights.crypto.CryptosBalancesInsightsResponse;
import com.distasilucas.cryptobalancetracker.model.response.insights.crypto.PageUserCryptosInsightsResponse;
import com.distasilucas.cryptobalancetracker.model.response.insights.crypto.PlatformInsight;
import com.distasilucas.cryptobalancetracker.model.response.insights.platform.PlatformInsightsResponse;
import com.distasilucas.cryptobalancetracker.model.response.insights.platform.PlatformsBalancesInsightsResponse;
import com.distasilucas.cryptobalancetracker.model.response.insights.platform.PlatformsInsights;
import com.distasilucas.cryptobalancetracker.repository.DateBalanceRepository;
import kotlin.Pair;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Clock;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static com.distasilucas.cryptobalancetracker.constants.Constants.CRYPTOS_BALANCES_INSIGHTS_CACHE;
import static com.distasilucas.cryptobalancetracker.constants.Constants.CRYPTO_INSIGHTS_CACHE;
import static com.distasilucas.cryptobalancetracker.constants.Constants.DATES_BALANCES_CACHE;
import static com.distasilucas.cryptobalancetracker.constants.Constants.PLATFORMS_BALANCES_INSIGHTS_CACHE;
import static com.distasilucas.cryptobalancetracker.constants.Constants.PLATFORM_INSIGHTS_CACHE;
import static com.distasilucas.cryptobalancetracker.constants.Constants.TOTAL_BALANCES_CACHE;
import static java.lang.Math.ceil;
@Slf4j
@Service
public class InsightsService {
private static final Double ELEMENTS_PER_PAGE = 10.0;
private static final int INT_ELEMENTS_PER_PAGE = ELEMENTS_PER_PAGE.intValue();
private final int max;
private final PlatformService platformService;
private final UserCryptoService userCryptoService;
private final CryptoService cryptoService;
private final DateBalanceRepository dateBalanceRepository;
private final Clock clock;
public InsightsService(@Value("${insights.cryptos}") int max,
PlatformService platformService,
UserCryptoService userCryptoService,
CryptoService cryptoService,
DateBalanceRepository dateBalanceRepository,
Clock clock) {
this.max = max;
this.platformService = platformService;
this.userCryptoService = userCryptoService;
this.cryptoService = cryptoService;
this.dateBalanceRepository = dateBalanceRepository;
this.clock = clock;
}
@Cacheable(cacheNames = TOTAL_BALANCES_CACHE)
public BalancesResponse retrieveTotalBalancesInsights() {
log.info("Retrieving total balances");
var userCryptos = userCryptoService.findAll();
if (userCryptos.isEmpty()) {
return BalancesResponse.empty();
}
var userCryptoQuantity = getUserCryptoQuantity(userCryptos);
var cryptosIds = userCryptos.stream().map(userCrypto -> userCrypto.getCrypto().getId()).collect(Collectors.toSet());
var cryptos = cryptoService.findAllByIds(cryptosIds);
return getTotalBalances(cryptos, userCryptoQuantity);
}
@Cacheable(cacheNames = DATES_BALANCES_CACHE, key = "#dateRange")
public DatesBalanceResponse retrieveDatesBalances(DateRange dateRange) {
log.info("Retrieving balances for date range: {}", dateRange);
List<DateBalance> dateBalances = new ArrayList<>();
var now = LocalDateTime.now(clock).toLocalDate();
switch (dateRange) {
case ONE_DAY -> dateBalances.addAll(retrieveDatesBalances(now.minusDays(2), now));
case THREE_DAYS -> dateBalances.addAll(retrieveDatesBalances(now.minusDays(3), now));
case ONE_WEEK -> dateBalances.addAll(retrieveDatesBalances(now.minusWeeks(1), now));
case ONE_MONTH -> dateBalances.addAll(retrieveDatesBalances(2, 4, now.minusMonths(1), now));
case THREE_MONTHS -> dateBalances.addAll(retrieveDatesBalances(6, 5, now.minusMonths(3), now));
case SIX_MONTHS -> dateBalances.addAll(retrieveDatesBalances(10, 6, now.minusMonths(6), now));
case ONE_YEAR -> dateBalances.addAll(retrieveYearDatesBalances(now));
}
var datesBalances = dateBalances
.stream()
.map(dateBalance -> {
String formattedDate = dateBalance.getDate().format(DateTimeFormatter.ofPattern("d MMMM yyyy"));
var balancesResponse = new BalancesResponse(dateBalance.getBalances());
return new DateBalances(formattedDate, balancesResponse);
})
.toList();
log.info("Balances found: {}", datesBalances.size());
if (datesBalances.isEmpty()) {
return DatesBalanceResponse.empty();
}
var changesPair = changesPair(datesBalances);
return new DatesBalanceResponse(datesBalances, changesPair.getFirst(), changesPair.getSecond());
}
@Cacheable(cacheNames = PLATFORM_INSIGHTS_CACHE, key = "#platformId")
public PlatformInsightsResponse retrievePlatformInsights(String platformId) {
log.info("Retrieving insights for platform with id {}", platformId);
var userCryptosInPlatform = userCryptoService.findAllByPlatformId(platformId);
if (userCryptosInPlatform.isEmpty()) {
return PlatformInsightsResponse.empty();
}
var platformResponse = platformService.retrievePlatformById(platformId);
var cryptosIds = userCryptosInPlatform.stream().map(userCrypto -> userCrypto.getCrypto().getId()).toList();
var cryptos = cryptoService.findAllByIds(cryptosIds);
var userCryptosQuantity = getUserCryptoQuantity(userCryptosInPlatform);
var totalBalances = getTotalBalances(cryptos, userCryptosQuantity);
var cryptosInsights = userCryptosInPlatform.stream()
.map(userCrypto -> {
var quantity = userCryptosQuantity.get(userCrypto.getCrypto().getId());
var crypto = cryptos.stream()
.filter(c -> userCrypto.getCrypto().getId().equals(c.getId()))
.findFirst()
.get();
var cryptoTotalBalances = getCryptoTotalBalances(crypto, quantity);
return new CryptoInsights(
userCrypto.getId(),
crypto.getCryptoInfo().getName(),
crypto.getId(),
quantity.toPlainString(),
cryptoTotalBalances,
calculatePercentage(totalBalances.totalUSDBalance(), cryptoTotalBalances.totalUSDBalance())
);
})
.sorted(Comparator.comparing(CryptoInsights::percentage, Comparator.reverseOrder()))
.toList();
return new PlatformInsightsResponse(platformResponse.getName(), totalBalances, cryptosInsights);
}
@Cacheable(cacheNames = CRYPTO_INSIGHTS_CACHE, key = "#coingeckoCryptoId")
public CryptoInsightResponse retrieveCryptoInsights(String coingeckoCryptoId) {
log.info("Retrieving insights for crypto with coingeckoCryptoId {}", coingeckoCryptoId);
var userCryptos = userCryptoService.findAllByCoingeckoCryptoId(coingeckoCryptoId);
if (userCryptos.isEmpty()) {
return CryptoInsightResponse.empty();
}
var platformsIds = userCryptos.stream().map(userCrypto -> userCrypto.getPlatform().getId()).toList();
var platforms = platformService.findAllByIds(platformsIds);
var crypto = cryptoService.retrieveCryptoInfoById(coingeckoCryptoId);
var platformUserCryptoQuantity = userCryptos.stream()
.collect(Collectors.toMap(userCrypto -> userCrypto.getPlatform().getId(), UserCrypto::getQuantity));
var totalCryptoQuantity = userCryptos.stream()
.map(UserCrypto::getQuantity)
.reduce(BigDecimal.ZERO, BigDecimal::add);
var totalBalances = getTotalBalances(List.of(crypto), Map.of(coingeckoCryptoId, totalCryptoQuantity));
var platformInsights = platforms.stream()
.map(platform -> {
var quantity = platformUserCryptoQuantity.get(platform.getId());
var cryptoTotalBalances = getCryptoTotalBalances(crypto, quantity);
return new PlatformInsight(
quantity.toPlainString(),
cryptoTotalBalances,
calculatePercentage(totalBalances.totalUSDBalance(), cryptoTotalBalances.totalUSDBalance()),
platform.getName()
);
})
.sorted(Comparator.comparing(PlatformInsight::percentage, Comparator.reverseOrder()))
.toList();
return new CryptoInsightResponse(crypto.getCryptoInfo().getName(), totalBalances, platformInsights);
}
@Cacheable(cacheNames = PLATFORMS_BALANCES_INSIGHTS_CACHE)
public PlatformsBalancesInsightsResponse retrievePlatformsBalancesInsights() {
log.info("Retrieving all platforms balances insights");
var userCryptos = userCryptoService.findAll();
if (userCryptos.isEmpty()) {
return PlatformsBalancesInsightsResponse.empty();
}
var platformsIds = userCryptos.stream()
.map(userCrypto -> userCrypto.getPlatform().getId())
.collect(Collectors.toSet());
var platforms = platformService.findAllByIds(platformsIds);
var userCryptoQuantity = getUserCryptoQuantity(userCryptos);
var platformsUserCryptos = getPlatformsUserCryptos(userCryptos, platforms);
var cryptosIds = platformsUserCryptos.values()
.stream()
.flatMap(cryptos -> cryptos.stream().map(userCrypto -> userCrypto.getCrypto().getId()))
.collect(Collectors.toSet());
var cryptos = cryptoService.findAllByIds(cryptosIds);
var totalBalances = getTotalBalances(cryptos, userCryptoQuantity);
var platformsInsights = platformsUserCryptos.entrySet()
.stream()
.map(entry -> {
var platformName = entry.getKey();
var cryptosUser = entry.getValue();
var totalUSDBalance = BigDecimal.ZERO;
var totalBTCBalance = BigDecimal.ZERO;
var totalEURBalance = BigDecimal.ZERO;
for (var userCrypto : cryptosUser) {
var crypto = cryptos.stream()
.filter(c -> c.getId().equalsIgnoreCase(userCrypto.getCrypto().getId()))
.findFirst()
.orElseThrow();
var balance = getCryptoTotalBalances(crypto, userCrypto.getQuantity());
totalUSDBalance = totalUSDBalance.add(new BigDecimal(balance.totalUSDBalance()));
totalBTCBalance = totalBTCBalance.add(new BigDecimal(balance.totalBTCBalance()));
totalEURBalance = totalEURBalance.add(new BigDecimal(balance.totalEURBalance()));
}
var balances = new BalancesResponse(
totalUSDBalance.toPlainString(),
totalEURBalance.toPlainString(),
totalBTCBalance.toPlainString()
);
var percentage = calculatePercentage(totalBalances.totalUSDBalance(), totalUSDBalance.toPlainString());
return new PlatformsInsights(platformName, balances, percentage);
})
.sorted(Comparator.comparing(PlatformsInsights::percentage, Comparator.reverseOrder()))
.toList();
return new PlatformsBalancesInsightsResponse(totalBalances, platformsInsights);
}
@Cacheable(cacheNames = CRYPTOS_BALANCES_INSIGHTS_CACHE)
public CryptosBalancesInsightsResponse retrieveCryptosBalancesInsights() {
log.info("Retrieving all cryptos balances insights");
var userCryptos = userCryptoService.findAll();
if (userCryptos.isEmpty()) {
return CryptosBalancesInsightsResponse.empty();
}
var userCryptoQuantity = getUserCryptoQuantity(userCryptos);
var cryptosIds = userCryptos.stream().map(userCrypto -> userCrypto.getCrypto().getId()).collect(Collectors.toSet());
var cryptos = cryptoService.findAllByIds(cryptosIds);
var totalBalances = getTotalBalances(cryptos, userCryptoQuantity);
var cryptosInsights = userCryptoQuantity.entrySet()
.stream()
.map(entry -> {
var coingeckoCryptoId = entry.getKey();
var quantity = entry.getValue();
var crypto = cryptos.stream()
.filter(c -> c.getId().equalsIgnoreCase(coingeckoCryptoId))
.findFirst()
.orElseThrow();
var cryptoBalances = getCryptoTotalBalances(crypto, quantity);
return new CryptoInsights(
crypto.getCryptoInfo().getName(),
coingeckoCryptoId,
quantity.toPlainString(),
cryptoBalances,
calculatePercentage(totalBalances.totalUSDBalance(), cryptoBalances.totalUSDBalance())
);
})
.sorted(Comparator.comparing(CryptoInsights::percentage, Comparator.reverseOrder()))
.toList();
var cryptosToReturn = cryptosInsights.size() > max ?
getCryptoInsightsWithOthers(totalBalances, cryptosInsights) :
cryptosInsights;
return new CryptosBalancesInsightsResponse(totalBalances, cryptosToReturn);
}
public Optional<PageUserCryptosInsightsResponse> retrieveUserCryptosInsights(int page, SortParams sortParams) {
log.info("Retrieving user cryptos insights for page {} with sort params {}", page, sortParams);
// Not the best because I'm paginating, but I need total balances to calculate individual percentages
var userCryptos = userCryptoService.findAll();
if (userCryptos.isEmpty()) {
return Optional.empty();
}
var cryptosIds = userCryptos.stream().map(userCrypto -> userCrypto.getCrypto().getId()).collect(Collectors.toSet());
var platformsIds = userCryptos.stream()
.map(userCrypto -> userCrypto.getPlatform().getId())
.collect(Collectors.toSet());
var cryptos = cryptoService.findAllByIds(cryptosIds);
var platforms = platformService.findAllByIds(platformsIds);
var userCryptoQuantity = getUserCryptoQuantity(userCryptos);
var totalBalances = getTotalBalances(cryptos, userCryptoQuantity);
List<UserCryptosInsights> userCryptosInsights = new ArrayList<>();
for (var userCrypto : userCryptos) {
var crypto = cryptos.stream()
.filter(c -> c.getId().equalsIgnoreCase(userCrypto.getCrypto().getId()))
.findFirst()
.orElseThrow();
var platform = platforms.stream()
.filter(p -> p.getId().equalsIgnoreCase(userCrypto.getPlatform().getId()))
.findFirst()
.orElseThrow();
var balances = getCryptoTotalBalances(crypto, userCrypto.getQuantity());
var circulatingSupply = getCirculatingSupply(crypto.getCryptoInfo().getMaxSupply(), crypto.getCryptoInfo().getCirculatingSupply());
var userCryptosInsight = new UserCryptosInsights(
userCrypto,
crypto,
calculatePercentage(totalBalances.totalUSDBalance(), balances.totalUSDBalance()),
balances,
new MarketData(circulatingSupply, crypto),
List.of(platform.getName())
);
userCryptosInsights.add(userCryptosInsight);
}
userCryptosInsights = userCryptosInsights.stream()
.sorted(sortParams.cryptosInsightsResponseComparator())
.toList();
var startIndex = page * INT_ELEMENTS_PER_PAGE;
if (startIndex > userCryptosInsights.size()) {
return Optional.empty();
}
var totalPages = (int) ceil(userCryptos.size() / ELEMENTS_PER_PAGE);
var endIndex = isLastPage(page, totalPages) ? userCryptosInsights.size() : startIndex + INT_ELEMENTS_PER_PAGE;
var cryptosInsights = userCryptosInsights.subList(startIndex, endIndex);
return Optional.of(new PageUserCryptosInsightsResponse(page, totalPages, totalBalances, cryptosInsights));
}
public Optional<PageUserCryptosInsightsResponse> retrieveUserCryptosPlatformsInsights(int page, SortParams sortParams) {
log.info("Retrieving user cryptos in platforms insights for page {} with sort params {}", page, sortParams);
// If one of the user cryptos happens to be at the end, and another of the same (i.e: bitcoin), at the start
// using findAllByPage() will display the same crypto twice (in this example), and the idea of this insight
// it's to display total balances and percentage for each individual crypto.
// So I need to calculate everything from all the user cryptos.
// Maybe create a query that returns the coingeckoCryptoId summing all balances for that crypto and
// returning an array of the platforms for that crypto and then paginate the results
// would be a better approach, so I don't need to retrieve all.
var userCryptos = userCryptoService.findAll();
if (userCryptos.isEmpty()) {
return Optional.empty();
}
var userCryptoQuantity = getUserCryptoQuantity(userCryptos);
var cryptosIds = userCryptos.stream().map(userCrypto -> userCrypto.getCrypto().getId()).collect(Collectors.toSet());
var cryptos = cryptoService.findAllByIds(cryptosIds);
var platformsIds = userCryptos.stream()
.map(userCrypto -> userCrypto.getPlatform().getId())
.collect(Collectors.toSet());
var platforms = platformService.findAllByIds(platformsIds);
var totalBalances = getTotalBalances(cryptos, userCryptoQuantity);
var userCryptosQuantityPlatforms = getUserCryptosQuantityPlatforms(userCryptos, platforms);
var userCryptosInsights = userCryptosQuantityPlatforms.entrySet()
.stream()
.map(entry -> {
var cryptoTotalQuantity = entry.getValue().component1();
var cryptoPlatforms = entry.getValue().component2();
var crypto = cryptos.stream()
.filter(c -> c.getId().equalsIgnoreCase(entry.getKey()))
.findFirst()
.orElseThrow();
var cryptoTotalBalances = getCryptoTotalBalances(crypto, cryptoTotalQuantity);
var circulatingSupply = getCirculatingSupply(crypto.getCryptoInfo().getMaxSupply(), crypto.getCryptoInfo().getCirculatingSupply());
return new UserCryptosInsights(
new CryptoInfo(crypto.getCryptoInfo().getName(), crypto.getId(), crypto.getCryptoInfo().getTicker(), crypto.getCryptoInfo().getImage()),
cryptoTotalQuantity.toPlainString(),
calculatePercentage(totalBalances.totalUSDBalance(), cryptoTotalBalances.totalUSDBalance()),
cryptoTotalBalances,
crypto.getCryptoInfo().getMarketCapRank(),
new MarketData(circulatingSupply, crypto),
cryptoPlatforms
);
})
.sorted(sortParams.cryptosInsightsResponseComparator())
.toList();
var startIndex = page * INT_ELEMENTS_PER_PAGE;
if (startIndex > userCryptosInsights.size()) {
return Optional.empty();
}
var totalPages = (int) ceil(userCryptosInsights.size() / ELEMENTS_PER_PAGE);
var endIndex = isLastPage(page, totalPages) ? userCryptosInsights.size() : startIndex + INT_ELEMENTS_PER_PAGE;
var cryptosInsights = userCryptosInsights.subList(startIndex, endIndex);
return Optional.of(new PageUserCryptosInsightsResponse(page, totalPages, totalBalances, cryptosInsights));
}
private Map<String, BigDecimal> getUserCryptoQuantity(List<UserCrypto> userCryptos) {
var userCryptoQuantity = new HashMap<String, BigDecimal>();
userCryptos.forEach(userCrypto -> {
if (userCryptoQuantity.containsKey(userCrypto.getCrypto().getId())) {
var quantity = userCryptoQuantity.get(userCrypto.getCrypto().getId());
userCryptoQuantity.put(userCrypto.getCrypto().getId(), quantity.add(userCrypto.getQuantity()));
} else {
userCryptoQuantity.put(userCrypto.getCrypto().getId(), userCrypto.getQuantity());
}
});
return userCryptoQuantity;
}
private BalancesResponse getTotalBalances(List<Crypto> cryptos, Map<String, BigDecimal> userCryptoQuantity) {
var totalUSDBalance = BigDecimal.ZERO;
var totalBTCBalance = BigDecimal.ZERO;
var totalEURBalance = BigDecimal.ZERO;
var entries = userCryptoQuantity.entrySet();
for (Map.Entry<String, BigDecimal> entry : entries) {
var coingeckoCryptoId = entry.getKey();
var quantity = entry.getValue();
var crypto = cryptos.stream()
.filter(c -> c.getId().equalsIgnoreCase(coingeckoCryptoId))
.findFirst()
.orElseThrow();
var lastKnownPrice = crypto.getLastKnownPrices().getLastKnownPrice();
var lastKnownPriceInBTC = crypto.getLastKnownPrices().getLastKnownPriceInBTC();
var lastKnownPriceInEUR = crypto.getLastKnownPrices().getLastKnownPriceInEUR();
totalUSDBalance = totalUSDBalance.add(lastKnownPrice.multiply(quantity).setScale(2, RoundingMode.HALF_UP));
totalBTCBalance = totalBTCBalance.add(lastKnownPriceInBTC.multiply(quantity)).stripTrailingZeros();
totalEURBalance = totalEURBalance.add(lastKnownPriceInEUR.multiply(quantity).setScale(2, RoundingMode.HALF_UP));
}
return new BalancesResponse(
totalUSDBalance.toPlainString(),
totalEURBalance.toPlainString(),
totalBTCBalance.setScale(10, RoundingMode.HALF_EVEN).stripTrailingZeros().toPlainString()
);
}
private BalancesResponse getCryptoTotalBalances(Crypto crypto, BigDecimal quantity) {
return new BalancesResponse(
crypto.getLastKnownPrices().getLastKnownPrice().multiply(quantity).setScale(2, RoundingMode.HALF_UP).toPlainString(),
crypto.getLastKnownPrices().getLastKnownPriceInEUR().multiply(quantity).setScale(2, RoundingMode.HALF_UP).toPlainString(),
crypto.getLastKnownPrices().getLastKnownPriceInBTC().multiply(quantity).setScale(10, RoundingMode.HALF_EVEN).stripTrailingZeros().toPlainString()
);
}
private CirculatingSupply getCirculatingSupply(BigDecimal maxSupply, BigDecimal circulatingSupply) {
var circulatingSupplyPercentage = 0f;
if (BigDecimal.ZERO.compareTo(maxSupply) < 0) {
circulatingSupplyPercentage = circulatingSupply.multiply(new BigDecimal("100"))
.divide(maxSupply, 2, RoundingMode.HALF_UP)
.floatValue();
}
return new CirculatingSupply(circulatingSupply.toPlainString(), circulatingSupplyPercentage);
}
private float calculatePercentage(String totalUSDBalance, String cryptoBalance) {
return new BigDecimal(cryptoBalance)
.multiply(new BigDecimal("100"))
.divide(new BigDecimal(totalUSDBalance), 2, RoundingMode.HALF_UP)
.floatValue();
}
private Map<String, List<UserCrypto>> getPlatformsUserCryptos(List<UserCrypto> userCryptos, List<Platform> platforms) {
var platformsUserCryptos = new HashMap<String, List<UserCrypto>>();
userCryptos.forEach(userCrypto -> {
var platform = platforms.stream()
.filter(p -> p.getId().equalsIgnoreCase(userCrypto.getPlatform().getId()))
.findFirst()
.orElseThrow();
if (platformsUserCryptos.containsKey(platform.getName())) {
var cryptos = new ArrayList<>(platformsUserCryptos.get(platform.getName()));
cryptos.add(userCrypto);
platformsUserCryptos.put(platform.getName(), cryptos);
} else {
platformsUserCryptos.put(platform.getName(), List.of(userCrypto));
}
});
return platformsUserCryptos;
}
private List<CryptoInsights> getCryptoInsightsWithOthers(BalancesResponse totalBalances, List<CryptoInsights> cryptosInsights) {
var topCryptos = cryptosInsights.subList(0, max);
var others = cryptosInsights.subList(max, cryptosInsights.size());
var totalUSDBalance = BigDecimal.ZERO;
var totalBTCBalance = BigDecimal.ZERO;
var totalEURBalance = BigDecimal.ZERO;
for (var cryptoInsights : others) {
totalUSDBalance = totalUSDBalance.add(new BigDecimal(cryptoInsights.balances().totalUSDBalance()));
totalBTCBalance = totalBTCBalance.add(new BigDecimal(cryptoInsights.balances().totalBTCBalance()));
totalEURBalance = totalEURBalance.add(new BigDecimal(cryptoInsights.balances().totalEURBalance()));
}
var othersTotalPercentage = calculatePercentage(totalBalances.totalUSDBalance(), totalUSDBalance.toPlainString());
var balancesResponse = new BalancesResponse(
totalUSDBalance.toPlainString(),
totalEURBalance.toPlainString(),
totalBTCBalance.toPlainString()
);
var othersCryptoInsights = new CryptoInsights("Others", balancesResponse, othersTotalPercentage);
List<CryptoInsights> cryptosInsightsWithOthers = new ArrayList<>(topCryptos);
cryptosInsightsWithOthers.add(othersCryptoInsights);
return cryptosInsightsWithOthers;
}
private Map<String, Pair<BigDecimal, List<String>>> getUserCryptosQuantityPlatforms(
List<UserCrypto> userCryptos,
List<Platform> platforms
) {
var map = new HashMap<String, Pair<BigDecimal, List<String>>>();
userCryptos.forEach(userCrypto -> {
var platformName = platforms.stream()
.filter(p -> p.getId().equalsIgnoreCase(userCrypto.getPlatform().getId()))
.findFirst()
.orElseThrow()
.getName();
if (map.containsKey(userCrypto.getCrypto().getId())) {
var crypto = map.get(userCrypto.getCrypto().getId());
var actualQuantity = crypto.component1();
var actualPlatforms = new ArrayList<>(crypto.component2());
var newQuantity = actualQuantity.add(userCrypto.getQuantity());
actualPlatforms.add(platformName);
map.put(userCrypto.getCrypto().getId(), new Pair<>(newQuantity, actualPlatforms));
} else {
map.put(userCrypto.getCrypto().getId(), new Pair<>(userCrypto.getQuantity(), List.of(platformName)));
}
});
return map;
}
private boolean isLastPage(int page, int totalPages) {
return page + 1 >= totalPages;
}
private List<DateBalance> retrieveDatesBalances(LocalDate from, LocalDate to) {
log.info("Retrieving date balances from {} to {}", from, to);
return dateBalanceRepository.findDateBalancesByDateBetween(from, to);
}
private List<DateBalance> retrieveDatesBalances(long daysSubtraction, int minRequired,
LocalDate from, LocalDate to) {
List<LocalDate> dates = new ArrayList<>();
while (from.isBefore(to)) {
dates.add(to);
to = to.minusDays(daysSubtraction);
}
log.info("Searching balances for dates {}", dates);
var datesBalances = dateBalanceRepository.findAllByDateIn(dates);
log.info("Found dates balances {}", datesBalances.stream().map(DateBalance::getDate).toList());
return datesBalances.size() >= minRequired ?
datesBalances :
retrieveLastTwelveDaysBalances();
}
private List<DateBalance> retrieveYearDatesBalances(LocalDate now) {
List<LocalDate> dates = new ArrayList<>();
dates.add(now);
IntStream.range(1, 12)
.forEach(n -> dates.add(now.minusMonths(n)));
log.info("Searching balances for dates {}", dates);
var datesBalances = dateBalanceRepository.findAllByDateIn(dates);
return datesBalances.size() > 3 ?
datesBalances :
retrieveLastTwelveDaysBalances();
}
private List<DateBalance> retrieveLastTwelveDaysBalances() {
var to = LocalDateTime.now(clock).toLocalDate();
var from = to.minusDays(12).atTime(23, 59, 59, 0).toLocalDate();
log.info("Not enough balances. Retrieving balances for the last twelve days from {} to {}", from, to);
return dateBalanceRepository.findDateBalancesByDateBetween(from, to);
}
private Pair<BalanceChanges, DifferencesChanges> changesPair(List<DateBalances> dateBalances) {
var usdChange = getChange(BalanceType.USD_BALANCE, dateBalances);
var eurChange = getChange(BalanceType.EUR_BALANCE, dateBalances);
var btcChange = getChange(BalanceType.BTC_BALANCE, dateBalances);
return new Pair<>(
new BalanceChanges(usdChange.getFirst(), eurChange.getFirst(), btcChange.getFirst()),
new DifferencesChanges(usdChange.getSecond(), eurChange.getSecond(), btcChange.getSecond())
);
}
private Pair<Float, String> getChange(BalanceType balanceType, List<DateBalances> dateBalances) {
var newestValues = dateBalances.getLast().balances();
var oldestValues = dateBalances.getFirst().balances();
var divisionScale = 4;
if (BalanceType.BTC_BALANCE == balanceType) divisionScale = 10;
var values = switch (balanceType) {
case USD_BALANCE -> new Pair<>(new BigDecimal(oldestValues.totalUSDBalance()), new BigDecimal(newestValues.totalUSDBalance()));
case EUR_BALANCE -> new Pair<>(new BigDecimal(oldestValues.totalEURBalance()), new BigDecimal(newestValues.totalEURBalance()));
case BTC_BALANCE -> new Pair<>(new BigDecimal(oldestValues.totalBTCBalance()), new BigDecimal(newestValues.totalBTCBalance()));
};
var newestValue = values.getSecond();
var oldestValue = values.getFirst();
var change = newestValue
.subtract(oldestValue)
.divide(oldestValue, divisionScale, RoundingMode.HALF_UP)
.multiply(new BigDecimal("100"))
.setScale(2, RoundingMode.HALF_UP)
.floatValue();
var difference = newestValue.subtract(oldestValue).toPlainString();
return new Pair<>(change, difference);
}
}