/* * 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.trades; import com.google.common.annotations.VisibleForTesting; import com.google.inject.Inject; import io.bitsquare.btc.pricefeed.PriceFeedService; import io.bitsquare.common.util.MathUtils; import io.bitsquare.gui.Navigation; import io.bitsquare.gui.common.model.ActivatableViewModel; import io.bitsquare.gui.main.MainView; import io.bitsquare.gui.main.market.trades.charts.CandleData; import io.bitsquare.gui.main.settings.SettingsView; import io.bitsquare.gui.main.settings.preferences.PreferencesView; import io.bitsquare.gui.util.BSFormatter; import io.bitsquare.gui.util.CurrencyListItem; import io.bitsquare.gui.util.GUIUtil; import io.bitsquare.locale.CryptoCurrency; import io.bitsquare.locale.CurrencyUtil; import io.bitsquare.locale.TradeCurrency; import io.bitsquare.trade.statistics.TradeStatistics; import io.bitsquare.trade.statistics.TradeStatisticsManager; import io.bitsquare.user.Preferences; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.collections.SetChangeListener; import javafx.scene.chart.XYChart; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; class TradesChartsViewModel extends ActivatableViewModel { private static final Logger log = LoggerFactory.getLogger(TradesChartsViewModel.class); private static final int TAB_INDEX = 2; /////////////////////////////////////////////////////////////////////////////////////////// // Enum /////////////////////////////////////////////////////////////////////////////////////////// public enum TickUnit { YEAR, MONTH, WEEK, DAY, HOUR, MINUTE_10, // TODO Can be removed after version 4.9.7 // Not used anymore but leave it as it might be used in preferences and could cause an exception if not there. MINUTE } private final TradeStatisticsManager tradeStatisticsManager; final Preferences preferences; private PriceFeedService priceFeedService; private Navigation navigation; private BSFormatter formatter; private final SetChangeListener<TradeStatistics> setChangeListener; final ObjectProperty<TradeCurrency> selectedTradeCurrencyProperty = new SimpleObjectProperty<>(); final BooleanProperty showAllTradeCurrenciesProperty = new SimpleBooleanProperty(false); private final ObservableList<CurrencyListItem> currencyListItems = FXCollections.observableArrayList(); private CurrencyListItem showAllCurrencyListItem = new CurrencyListItem(new CryptoCurrency(GUIUtil.SHOW_ALL_FLAG, GUIUtil.SHOW_ALL_FLAG), -1); final ObservableList<TradeStatistics> tradeStatisticsByCurrency = FXCollections.observableArrayList(); ObservableList<XYChart.Data<Number, Number>> priceItems = FXCollections.observableArrayList(); ObservableList<XYChart.Data<Number, Number>> volumeItems = FXCollections.observableArrayList(); TickUnit tickUnit = TickUnit.DAY; int maxTicks = 30; private int selectedTabIndex; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// @Inject public TradesChartsViewModel(TradeStatisticsManager tradeStatisticsManager, Preferences preferences, PriceFeedService priceFeedService, Navigation navigation, BSFormatter formatter) { this.tradeStatisticsManager = tradeStatisticsManager; this.preferences = preferences; this.priceFeedService = priceFeedService; this.navigation = navigation; this.formatter = formatter; setChangeListener = change -> { updateChartData(); fillTradeCurrencies(); }; Optional<TradeCurrency> tradeCurrencyOptional = CurrencyUtil.getTradeCurrency(preferences.getTradeChartsScreenCurrencyCode()); if (tradeCurrencyOptional.isPresent()) selectedTradeCurrencyProperty.set(tradeCurrencyOptional.get()); else selectedTradeCurrencyProperty.set(CurrencyUtil.getDefaultTradeCurrency()); tickUnit = TickUnit.values()[preferences.getTradeStatisticsTickUnitIndex()]; } private void fillTradeCurrencies() { // Don't use a set as we need all entries List<TradeCurrency> tradeCurrencyList = tradeStatisticsManager.getObservableTradeStatisticsSet().stream() .map(e -> { Optional<TradeCurrency> tradeCurrencyOptional = CurrencyUtil.getTradeCurrency(e.currency); if (tradeCurrencyOptional.isPresent()) return tradeCurrencyOptional.get(); else return null; }) .filter(e -> e != null) .collect(Collectors.toList()); GUIUtil.fillCurrencyListItems(tradeCurrencyList, currencyListItems, showAllCurrencyListItem, preferences); } @VisibleForTesting TradesChartsViewModel() { setChangeListener = null; preferences = null; tradeStatisticsManager = null; } @Override protected void activate() { tradeStatisticsManager.getObservableTradeStatisticsSet().addListener(setChangeListener); fillTradeCurrencies(); updateChartData(); syncPriceFeedCurrency(); setMarketPriceFeedCurrency(); } @Override protected void deactivate() { tradeStatisticsManager.getObservableTradeStatisticsSet().removeListener(setChangeListener); } /////////////////////////////////////////////////////////////////////////////////////////// // UI actions /////////////////////////////////////////////////////////////////////////////////////////// void onSetTradeCurrency(TradeCurrency tradeCurrency) { if (tradeCurrency != null) { final String code = tradeCurrency.getCode(); if (isEditEntry(code)) { navigation.navigateTo(MainView.class, SettingsView.class, PreferencesView.class); } else { boolean showAllEntry = isShowAllEntry(code); showAllTradeCurrenciesProperty.set(showAllEntry); if (!showAllEntry) { selectedTradeCurrencyProperty.set(tradeCurrency); preferences.setTradeChartsScreenCurrencyCode(code); } updateChartData(); if (!preferences.getUseStickyMarketPrice()) { if (showAllEntry) priceFeedService.setCurrencyCode(CurrencyUtil.getDefaultTradeCurrency().getCode()); else priceFeedService.setCurrencyCode(code); } } } } void setTickUnit(TickUnit tickUnit) { this.tickUnit = tickUnit; preferences.setTradeStatisticsTickUnitIndex(tickUnit.ordinal()); updateChartData(); } void setSelectedTabIndex(int selectedTabIndex) { this.selectedTabIndex = selectedTabIndex; syncPriceFeedCurrency(); setMarketPriceFeedCurrency(); } /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// public String getCurrencyCode() { return selectedTradeCurrencyProperty.get().getCode(); } public ObservableList<CurrencyListItem> getCurrencyListItems() { return currencyListItems; } public Optional<CurrencyListItem> getSelectedCurrencyListItem() { return currencyListItems.stream().filter(e -> e.tradeCurrency.equals(selectedTradeCurrencyProperty.get())).findAny(); } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private void setMarketPriceFeedCurrency() { if (!preferences.getUseStickyMarketPrice() && selectedTabIndex == TAB_INDEX) { if (showAllTradeCurrenciesProperty.get()) priceFeedService.setCurrencyCode(CurrencyUtil.getDefaultTradeCurrency().getCode()); else priceFeedService.setCurrencyCode(getCurrencyCode()); } } private void syncPriceFeedCurrency() { if (!preferences.getUseStickyMarketPrice() && selectedTabIndex == TAB_INDEX) priceFeedService.setCurrencyCode(selectedTradeCurrencyProperty.get().getCode()); } private void updateChartData() { tradeStatisticsByCurrency.setAll(tradeStatisticsManager.getObservableTradeStatisticsSet().stream() .filter(e -> showAllTradeCurrenciesProperty.get() || e.currency.equals(getCurrencyCode())) .collect(Collectors.toList())); // Get all entries for the defined time interval Map<Long, Set<TradeStatistics>> itemsPerInterval = new HashMap<>(); final long dateAsTime = new Date().getTime(); tradeStatisticsByCurrency.stream().forEach(e -> { Set<TradeStatistics> set; final long time = getTickFromTime(e.tradeDate, tickUnit); final long now = getTickFromTime(dateAsTime, tickUnit); long index = maxTicks - (now - time); if (itemsPerInterval.containsKey(index)) { set = itemsPerInterval.get(index); } else { set = new HashSet<>(); itemsPerInterval.put(index, set); } set.add(e); }); // create CandleData for defined time interval List<CandleData> candleDataList = itemsPerInterval.entrySet().stream() .filter(entry -> entry.getKey() >= 0) .map(entry -> getCandleData(entry.getKey(), entry.getValue())) .collect(Collectors.toList()); candleDataList.sort((o1, o2) -> (o1.tick < o2.tick ? -1 : (o1.tick == o2.tick ? 0 : 1))); priceItems.setAll(candleDataList.stream() .map(e -> new XYChart.Data<Number, Number>(e.tick, e.open, e)) .collect(Collectors.toList())); volumeItems.setAll(candleDataList.stream() .map(e -> new XYChart.Data<Number, Number>(e.tick, e.accumulatedAmount, e)) .collect(Collectors.toList())); } @VisibleForTesting CandleData getCandleData(long tick, Set<TradeStatistics> set) { long open = 0; long close = 0; long high = 0; long low = 0; long accumulatedVolume = 0; long accumulatedAmount = 0; long numTrades = set.size(); for (TradeStatistics item : set) { long tradePriceAsLong = item.tradePrice; if (CurrencyUtil.isCryptoCurrency(getCurrencyCode())) { low = (low != 0) ? Math.max(low, tradePriceAsLong) : tradePriceAsLong; high = (high != 0) ? Math.min(high, tradePriceAsLong) : tradePriceAsLong; } else { low = (low != 0) ? Math.min(low, tradePriceAsLong) : tradePriceAsLong; high = (high != 0) ? Math.max(high, tradePriceAsLong) : tradePriceAsLong; } accumulatedVolume += (item.getTradeVolume() != null) ? item.getTradeVolume().value : 0; accumulatedAmount += item.tradeAmount; } // 100000000 -> Coin.COIN.value; final double value = MathUtils.scaleUpByPowerOf10(accumulatedVolume, 8); long averagePrice = MathUtils.roundDoubleToLong(value / (double) accumulatedAmount); List<TradeStatistics> list = new ArrayList<>(set); list.sort((o1, o2) -> (o1.tradeDate < o2.tradeDate ? -1 : (o1.tradeDate == o2.tradeDate ? 0 : 1))); if (list.size() > 0) { open = list.get(0).tradePrice; close = list.get(list.size() - 1).tradePrice; } boolean isBullish = close > open; final Date dateFrom = new Date(getTimeFromTickIndex(tick)); final Date dateTo = new Date(getTimeFromTickIndex(tick + 1)); String dateString = tickUnit.ordinal() > TickUnit.DAY.ordinal() ? formatter.formatDateTimeSpan(dateFrom, dateTo) : formatter.formatDate(dateFrom) + " - " + formatter.formatDate(dateTo); if (CurrencyUtil.isCryptoCurrency(getCurrencyCode())) { return new CandleData(tick, getInvertedPrice(open), getInvertedPrice(close), getInvertedPrice(high), getInvertedPrice(low), getInvertedPrice(averagePrice), accumulatedAmount, accumulatedVolume, numTrades, isBullish, dateString); } else { return new CandleData(tick, open, close, high, low, averagePrice, accumulatedAmount, accumulatedVolume, numTrades, isBullish, dateString); } } long getInvertedPrice(long price) { final double value = price != 0 ? 1000000000000D / price : 0; return MathUtils.roundDoubleToLong(value); } long getTickFromTime(long tradeDateAsTime, TickUnit tickUnit) { switch (tickUnit) { case YEAR: return TimeUnit.MILLISECONDS.toDays(tradeDateAsTime) / 365; case MONTH: return TimeUnit.MILLISECONDS.toDays(tradeDateAsTime) / 31; case WEEK: return TimeUnit.MILLISECONDS.toDays(tradeDateAsTime) / 7; case DAY: return TimeUnit.MILLISECONDS.toDays(tradeDateAsTime); case HOUR: return TimeUnit.MILLISECONDS.toHours(tradeDateAsTime); case MINUTE_10: return TimeUnit.MILLISECONDS.toMinutes(tradeDateAsTime) / 10; case MINUTE: return TimeUnit.MILLISECONDS.toMinutes(tradeDateAsTime); default: return tradeDateAsTime; } } long getTimeFromTick(long tick, TickUnit tickUnit) { switch (tickUnit) { case YEAR: return TimeUnit.DAYS.toMillis(tick) * 365; case MONTH: return TimeUnit.DAYS.toMillis(tick) * 31; case WEEK: return TimeUnit.DAYS.toMillis(tick) * 7; case DAY: return TimeUnit.DAYS.toMillis(tick); case HOUR: return TimeUnit.HOURS.toMillis(tick); case MINUTE_10: return TimeUnit.MINUTES.toMillis(tick) * 10; case MINUTE: return TimeUnit.MINUTES.toMillis(tick); default: return tick; } } long getTimeFromTickIndex(long index) { long now = getTickFromTime(new Date().getTime(), tickUnit); long tick = now - (maxTicks - index); return getTimeFromTick(tick, tickUnit); } private boolean isShowAllEntry(String id) { return id.equals(GUIUtil.SHOW_ALL_FLAG); } private boolean isEditEntry(String id) { return id.equals(GUIUtil.EDIT_FLAG); } }