/* * This file is part of Bitsquare. * * Bitsquare is free software: you can redistribute it and/or modify it * under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or (at * your option) any later version. * * Bitsquare is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Bitsquare. If not, see <http://www.gnu.org/licenses/>. */ package io.bitsquare.gui.main.market.spread; import com.google.inject.Inject; import io.bitsquare.btc.pricefeed.MarketPrice; import io.bitsquare.btc.pricefeed.PriceFeedService; import io.bitsquare.gui.common.model.ActivatableViewModel; import io.bitsquare.gui.main.offer.offerbook.OfferBook; import io.bitsquare.gui.main.offer.offerbook.OfferBookListItem; import io.bitsquare.gui.main.overlays.popups.Popup; import io.bitsquare.gui.util.BSFormatter; import io.bitsquare.locale.CurrencyUtil; import io.bitsquare.trade.offer.Offer; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import org.bitcoinj.core.Coin; import org.bitcoinj.utils.Fiat; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; class SpreadViewModel extends ActivatableViewModel { private final OfferBook offerBook; private PriceFeedService priceFeedService; private BSFormatter formatter; private final ObservableList<OfferBookListItem> offerBookListItems; private final ListChangeListener<OfferBookListItem> listChangeListener; final ObservableList<SpreadItem> spreadItems = FXCollections.observableArrayList(); /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// @Inject public SpreadViewModel(OfferBook offerBook, PriceFeedService priceFeedService, BSFormatter formatter) { this.offerBook = offerBook; this.priceFeedService = priceFeedService; this.formatter = formatter; offerBookListItems = offerBook.getOfferBookListItems(); listChangeListener = c -> update(offerBookListItems); } @Override protected void activate() { offerBookListItems.addListener(listChangeListener); offerBook.fillOfferBookListItems(); update(offerBookListItems); } @Override protected void deactivate() { offerBookListItems.removeListener(listChangeListener); } private void update(ObservableList<OfferBookListItem> offerBookListItems) { Map<String, List<Offer>> offersByCurrencyMap = new HashMap<>(); for (OfferBookListItem offerBookListItem : offerBookListItems) { Offer offer = offerBookListItem.getOffer(); String currencyCode = offer.getCurrencyCode(); if (!offersByCurrencyMap.containsKey(currencyCode)) offersByCurrencyMap.put(currencyCode, new ArrayList<>()); offersByCurrencyMap.get(currencyCode).add(offer); } spreadItems.clear(); for (String currencyCode : offersByCurrencyMap.keySet()) { List<Offer> offers = offersByCurrencyMap.get(currencyCode); List<Offer> buyOffers = offers .stream() .filter(e -> e.getDirection().equals(Offer.Direction.BUY)) .sorted((o1, o2) -> { long a = o1.getPrice() != null ? o1.getPrice().value : 0; long b = o2.getPrice() != null ? o2.getPrice().value : 0; if (a != b) return a < b ? 1 : -1; return 0; }) .collect(Collectors.toList()); List<Offer> sellOffers = offers .stream() .filter(e -> e.getDirection().equals(Offer.Direction.SELL)) .sorted((o1, o2) -> { long a = o1.getPrice() != null ? o1.getPrice().value : 0; long b = o2.getPrice() != null ? o2.getPrice().value : 0; if (a != b) return a > b ? 1 : -1; return 0; }) .collect(Collectors.toList()); Fiat spread = null; String percentage = ""; Fiat bestSellOfferPrice = sellOffers.isEmpty() ? null : sellOffers.get(0).getPrice(); Fiat bestBuyOfferPrice = buyOffers.isEmpty() ? null : buyOffers.get(0).getPrice(); if (bestBuyOfferPrice != null && bestSellOfferPrice != null) { MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); // There have been some bug reports that an offer caused an overflow exception. // We never found out which offer it was. So add here a try/catch to get better info if it // happens again try { spread = bestSellOfferPrice.subtract(bestBuyOfferPrice); if (spread != null && marketPrice != null) { double marketPriceAsDouble = marketPrice.getPrice(PriceFeedService.Type.LAST); if (CurrencyUtil.isFiatCurrency(currencyCode)) { double result = ((double) spread.value / 10000D) / marketPriceAsDouble; percentage = " (" + formatter.formatPercentagePrice(result) + ")"; } else { final double spreadAsDouble = spread.value != 0 ? 10000D / spread.value : 0; double result = marketPriceAsDouble / spreadAsDouble; percentage = " (" + formatter.formatPercentagePrice(result) + ")"; } } } catch (Throwable t) { try { String msg = "An error occurred at the spread calculation.\n" + "Error msg: " + t.toString() + "\n" + "Details of offer data: \n" + "bestSellOfferPrice: " + bestSellOfferPrice.value + "\n" + "bestBuyOfferPrice: " + bestBuyOfferPrice.value + "\n" + "sellOffer getCurrencyCode: " + sellOffers.get(0).getCurrencyCode() + "\n" + "buyOffer getCurrencyCode: " + buyOffers.get(0).getCurrencyCode() + "\n\n" + "Please copy and paste this data and send it to the developers so they can investigate the issue."; new Popup().error(msg).show(); log.error(t.toString()); t.printStackTrace(); } catch (Throwable t2) { log.error(t2.toString()); t2.printStackTrace(); } } } Coin totalAmount = Coin.valueOf(offers.stream().mapToLong(offer -> offer.getAmount().getValue()).sum()); spreadItems.add(new SpreadItem(currencyCode, buyOffers.size(), sellOffers.size(), offers.size(), spread, percentage, totalAmount)); } } }