/* * 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.offerbook; import com.google.common.math.LongMath; import com.google.inject.Inject; import io.bitsquare.btc.pricefeed.PriceFeedService; import io.bitsquare.gui.Navigation; import io.bitsquare.gui.common.model.ActivatableViewModel; import io.bitsquare.gui.main.MainView; import io.bitsquare.gui.main.offer.offerbook.OfferBook; import io.bitsquare.gui.main.offer.offerbook.OfferBookListItem; import io.bitsquare.gui.main.settings.SettingsView; import io.bitsquare.gui.main.settings.preferences.PreferencesView; import io.bitsquare.gui.util.CurrencyListItem; import io.bitsquare.gui.util.GUIUtil; import io.bitsquare.locale.CurrencyUtil; import io.bitsquare.locale.TradeCurrency; import io.bitsquare.trade.offer.Offer; import io.bitsquare.user.Preferences; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.scene.chart.XYChart; import org.bitcoinj.utils.Fiat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; class OfferBookChartViewModel extends ActivatableViewModel { private static final Logger log = LoggerFactory.getLogger(OfferBookChartViewModel.class); private static final int TAB_INDEX = 0; private final OfferBook offerBook; final Preferences preferences; final PriceFeedService priceFeedService; private Navigation navigation; final ObjectProperty<TradeCurrency> selectedTradeCurrencyProperty = new SimpleObjectProperty<>(); private final List<XYChart.Data> buyData = new ArrayList<>(); private final List<XYChart.Data> sellData = new ArrayList<>(); private final ObservableList<OfferBookListItem> offerBookListItems; private final ListChangeListener<OfferBookListItem> offerBookListItemsListener; final ObservableList<CurrencyListItem> currencyListItems = FXCollections.observableArrayList(); private final ObservableList<OfferListItem> topBuyOfferList = FXCollections.observableArrayList(); private final ObservableList<OfferListItem> topSellOfferList = FXCollections.observableArrayList(); private final ChangeListener<Number> currenciesUpdatedListener; private int selectedTabIndex; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// @Inject public OfferBookChartViewModel(OfferBook offerBook, Preferences preferences, PriceFeedService priceFeedService, Navigation navigation) { this.offerBook = offerBook; this.preferences = preferences; this.priceFeedService = priceFeedService; this.navigation = navigation; Optional<TradeCurrency> tradeCurrencyOptional = CurrencyUtil.getTradeCurrency(preferences.getOfferBookChartScreenCurrencyCode()); if (tradeCurrencyOptional.isPresent()) selectedTradeCurrencyProperty.set(tradeCurrencyOptional.get()); else { selectedTradeCurrencyProperty.set(CurrencyUtil.getDefaultTradeCurrency()); } offerBookListItems = offerBook.getOfferBookListItems(); offerBookListItemsListener = c -> { c.next(); if (c.wasAdded() || c.wasRemoved()) { ArrayList<OfferBookListItem> list = new ArrayList<>(c.getRemoved()); list.addAll(c.getAddedSubList()); if (list.stream() .map(OfferBookListItem::getOffer) .filter(e -> e.getCurrencyCode().equals(selectedTradeCurrencyProperty.get().getCode())) .findAny() .isPresent()) updateChartData(); } fillTradeCurrencies(); }; currenciesUpdatedListener = new ChangeListener<Number>() { @Override public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) { if (!isAnyPricePresent()) { offerBook.fillOfferBookListItems(); updateChartData(); priceFeedService.currenciesUpdateFlagProperty().removeListener(currenciesUpdatedListener); } } }; } private void fillTradeCurrencies() { // Don't use a set as we need all entries List<TradeCurrency> tradeCurrencyList = offerBookListItems.stream() .map(e -> { Optional<TradeCurrency> tradeCurrencyOptional = CurrencyUtil.getTradeCurrency(e.getOffer().getCurrencyCode()); if (tradeCurrencyOptional.isPresent()) return tradeCurrencyOptional.get(); else return null; }) .filter(e -> e != null) .collect(Collectors.toList()); GUIUtil.fillCurrencyListItems(tradeCurrencyList, currencyListItems, null, preferences); } @Override protected void activate() { priceFeedService.setType(PriceFeedService.Type.LAST); offerBookListItems.addListener(offerBookListItemsListener); offerBook.fillOfferBookListItems(); fillTradeCurrencies(); updateChartData(); if (isAnyPricePresent()) priceFeedService.currenciesUpdateFlagProperty().addListener(currenciesUpdatedListener); syncPriceFeedCurrency(); } @Override protected void deactivate() { offerBookListItems.removeListener(offerBookListItemsListener); } /////////////////////////////////////////////////////////////////////////////////////////// // UI actions /////////////////////////////////////////////////////////////////////////////////////////// public void onSetTradeCurrency(TradeCurrency tradeCurrency) { if (tradeCurrency != null) { final String code = tradeCurrency.getCode(); if (isEditEntry(code)) { navigation.navigateTo(MainView.class, SettingsView.class, PreferencesView.class); } else { selectedTradeCurrencyProperty.set(tradeCurrency); preferences.setOfferBookChartScreenCurrencyCode(code); updateChartData(); if (!preferences.getUseStickyMarketPrice()) priceFeedService.setCurrencyCode(code); } } } void setSelectedTabIndex(int selectedTabIndex) { this.selectedTabIndex = selectedTabIndex; syncPriceFeedCurrency(); } /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// public List<XYChart.Data> getBuyData() { return buyData; } public List<XYChart.Data> getSellData() { return sellData; } public String getCurrencyCode() { return selectedTradeCurrencyProperty.get().getCode(); } public ObservableList<OfferBookListItem> getOfferBookListItems() { return offerBookListItems; } public ObservableList<OfferListItem> getTopBuyOfferList() { return topBuyOfferList; } public ObservableList<OfferListItem> getTopSellOfferList() { return topSellOfferList; } public ObservableList<CurrencyListItem> getCurrencyListItems() { return currencyListItems; } public Optional<CurrencyListItem> getSelectedCurrencyListItem() { return currencyListItems.stream().filter(e -> e.tradeCurrency.equals(selectedTradeCurrencyProperty.get())).findAny(); } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private void syncPriceFeedCurrency() { if (!preferences.getUseStickyMarketPrice() && selectedTabIndex == TAB_INDEX) priceFeedService.setCurrencyCode(selectedTradeCurrencyProperty.get().getCode()); } private boolean isAnyPricePresent() { return offerBookListItems.stream().filter(item -> item.getOffer().getPrice() == null).findAny().isPresent(); } private void updateChartData() { List<Offer> allBuyOffers = offerBookListItems.stream() .map(OfferBookListItem::getOffer) .filter(e -> e.getCurrencyCode().equals(selectedTradeCurrencyProperty.get().getCode()) && 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()); allBuyOffers = filterOffersWithRelevantPrices(allBuyOffers); buildChartAndTableEntries(allBuyOffers, Offer.Direction.BUY, buyData, topBuyOfferList); List<Offer> allSellOffers = offerBookListItems.stream() .map(OfferBookListItem::getOffer) .filter(e -> e.getCurrencyCode().equals(selectedTradeCurrencyProperty.get().getCode()) && 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()); allSellOffers = filterOffersWithRelevantPrices(allSellOffers); buildChartAndTableEntries(allSellOffers, Offer.Direction.SELL, sellData, topSellOfferList); } // If there are more then 3 offers we ignore the offers which are further than 30% from the best price private List<Offer> filterOffersWithRelevantPrices(List<Offer> offers) { if (offers.size() > 3) { Fiat bestPrice = offers.get(0).getPrice(); if (bestPrice != null) { long bestPriceAsLong = bestPrice.longValue(); return offers.stream() .filter(e -> { if (e.getPrice() == null) return false; double ratio = (double) e.getPrice().longValue() / (double) bestPriceAsLong; return Math.abs(1 - ratio) < 0.3; }) .collect(Collectors.toList()); } } return offers; } private void buildChartAndTableEntries(List<Offer> sortedList, Offer.Direction direction, List<XYChart.Data> data, ObservableList<OfferListItem> offerTableList) { data.clear(); double accumulatedAmount = 0; List<OfferListItem> offerTableListTemp = new ArrayList<>(); for (Offer offer : sortedList) { Fiat priceAsFiat = offer.getPrice(); if (priceAsFiat != null) { double amount = (double) offer.getAmount().value / LongMath.pow(10, offer.getAmount().smallestUnitExponent()); accumulatedAmount += amount; offerTableListTemp.add(new OfferListItem(offer, accumulatedAmount)); double price = (double) priceAsFiat.value / LongMath.pow(10, priceAsFiat.smallestUnitExponent()); if (CurrencyUtil.isCryptoCurrency(getCurrencyCode())) { price = price != 0 ? 1d / price : 0; if (direction.equals(Offer.Direction.SELL)) data.add(0, new XYChart.Data<>(price, accumulatedAmount)); else data.add(new XYChart.Data<>(price, accumulatedAmount)); } else { if (direction.equals(Offer.Direction.BUY)) data.add(0, new XYChart.Data<>(price, accumulatedAmount)); else data.add(new XYChart.Data<>(price, accumulatedAmount)); } } } offerTableList.setAll(offerTableListTemp); } private boolean isEditEntry(String id) { return id.equals(GUIUtil.EDIT_FLAG); } }