Show latest trade price as market price for currencies which have no external price feed provided but have been traded in Bisq

This commit is contained in:
Manfred Karrer 2017-07-26 11:44:28 +02:00
parent 03c33964a7
commit 1857cc0b30
13 changed files with 102 additions and 39 deletions

View file

@ -210,6 +210,9 @@ mainView.menu.account=Account
mainView.menu.dao=DAO
mainView.marketPrice.provider=Market price provider:
mainView.marketPrice.bisqInternalPrice=Price of latest Bisq trade
mainView.marketPrice.tooltip.bisqInternalPrice=There is no market price from external price feed providers available.\n\
The displayed price is the latest Bisq trade price for that currency.
mainView.marketPrice.tooltip=Market price is provided by {0}{1}\nLast update: {2}\nProvider node URL: {3}
mainView.marketPrice.tooltip.altcoinExtra=If the altcoin is not available at Poloniex we use https://coinmarketcap.com
mainView.balance.available=Available balance

View file

@ -85,7 +85,7 @@ public class BsqLiteNode extends BsqNode {
@Override
public void onBlockReceived(GetBsqBlocksResponse getBsqBlocksResponse) {
List<BsqBlock> bsqBlockList = new ArrayList<>(getBsqBlocksResponse.getBsqBlocks());
log.info("received msg with {} items", bsqBlockList.size(), bsqBlockList.get(bsqBlockList.size() - 1).getHeight());
log.info("received msg with {} items", bsqBlockList.size());
if (bsqBlockList.size() > 0)
log.info("block height of last item: {}", bsqBlockList.get(bsqBlockList.size() - 1).getHeight());
// Be safe and reset all mutable data in case the provider would not have done it

View file

@ -128,7 +128,7 @@ public class Offer implements NetworkPayload, PersistablePayload {
if (offerPayload.isUseMarketBasedPrice()) {
checkNotNull(priceFeedService, "priceFeed must not be null");
MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode);
if (marketPrice != null && marketPrice.isValid()) {
if (marketPrice != null && marketPrice.isRecentExternalPriceAvailable()) {
double factor;
double marketPriceMargin = offerPayload.getMarketPriceMargin();
if (CurrencyUtil.isCryptoCurrency(currencyCode)) {

View file

@ -27,15 +27,24 @@ public class MarketPrice {
private final String currencyCode;
private final double price;
private final long timestampSec;
private final boolean isExternallyProvidedPrice; // if we get it from btc average or others.
public MarketPrice(String currencyCode, double price, long timestampSec) {
public MarketPrice(String currencyCode, double price, long timestampSec, boolean isExternallyProvidedPrice) {
this.currencyCode = currencyCode;
this.price = price;
this.timestampSec = timestampSec;
this.isExternallyProvidedPrice = isExternallyProvidedPrice;
}
public boolean isValid() {
long limit = Instant.now().getEpochSecond() - MARKET_PRICE_MAX_AGE_SEC;
return timestampSec > limit && price > 0;
public boolean isPriceAvailable() {
return price > 0;
}
private boolean isRecentPriceAvailable() {
return timestampSec > (Instant.now().getEpochSecond() - MARKET_PRICE_MAX_AGE_SEC) && isPriceAvailable();
}
public boolean isRecentExternalPriceAvailable() {
return isExternallyProvidedPrice && isRecentPriceAvailable();
}
}

View file

@ -25,6 +25,8 @@ import io.bisq.common.app.Log;
import io.bisq.common.handlers.FaultHandler;
import io.bisq.common.locale.CurrencyUtil;
import io.bisq.common.locale.TradeCurrency;
import io.bisq.common.monetary.Price;
import io.bisq.common.util.MathUtils;
import io.bisq.common.util.Tuple2;
import io.bisq.core.app.BisqEnvironment;
import io.bisq.core.provider.ProvidersRepository;
@ -135,10 +137,21 @@ public class PriceFeedService {
public MarketPrice getMarketPrice(String currencyCode) {
if (cache.containsKey(currencyCode))
return cache.get(currencyCode);
else
else
return null;
}
public void setBisqMarketPrice(String currencyCode, Price price) {
if (!cache.containsKey(currencyCode) || !cache.get(currencyCode).isExternallyProvidedPrice()) {
cache.put(currencyCode, new MarketPrice(currencyCode,
MathUtils.scaleDownByPowerOf10(price.getValue(), CurrencyUtil.isCryptoCurrency(currencyCode) ? 8 : 4),
0,
false));
updateCounter.set(updateCounter.get() + 1);
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Setter
///////////////////////////////////////////////////////////////////////////////////////////
@ -198,7 +211,7 @@ public class PriceFeedService {
if (cache.containsKey(currencyCode)) {
try {
MarketPrice marketPrice = cache.get(currencyCode);
if (marketPrice.isValid())
if (marketPrice.isRecentExternalPriceAvailable())
priceConsumer.accept(marketPrice.getPrice());
} catch (Throwable t) {
log.warn("Error at applyPriceToConsumer " + t.getMessage());
@ -243,7 +256,7 @@ public class PriceFeedService {
convertedPrice = value.getPrice() / baseCurrencyPrice.getPrice();
else
convertedPrice = value.getPrice() * baseCurrencyPrice.getPrice();
convertedPriceMap.put(e.getKey(), new MarketPrice(value.getCurrencyCode(), convertedPrice, value.getTimestampSec()));
convertedPriceMap.put(e.getKey(), new MarketPrice(value.getCurrencyCode(), convertedPrice, value.getTimestampSec(), true));
});
cache.putAll(convertedPriceMap);
break;

View file

@ -58,7 +58,7 @@ public class PriceProvider extends HttpClientProvider {
final double price = (double) treeMap.get("price");
// json uses double for our timestampSec long value...
final long timestampSec = MathUtils.doubleToLong((double) treeMap.get("timestampSec"));
marketPriceMap.put(currencyCode, new MarketPrice(currencyCode, price, timestampSec));
marketPriceMap.put(currencyCode, new MarketPrice(currencyCode, price, timestampSec, true));
} catch (Throwable t) {
log.error(t.toString());
t.printStackTrace();

View file

@ -13,6 +13,7 @@ import io.bisq.common.storage.Storage;
import io.bisq.common.util.Utilities;
import io.bisq.core.app.AppOptionKeys;
import io.bisq.core.app.BisqEnvironment;
import io.bisq.core.provider.price.PriceFeedService;
import io.bisq.network.p2p.P2PService;
import io.bisq.network.p2p.storage.HashMapChangedListener;
import io.bisq.network.p2p.storage.payload.ProtectedStorageEntry;
@ -32,6 +33,7 @@ public class TradeStatisticsManager implements PersistedDataHost {
private final Storage<TradeStatisticsList> statisticsStorage;
private final JsonFileManager jsonFileManager;
private final P2PService p2PService;
private final PriceFeedService priceFeedService;
private final boolean dumpStatistics;
private final ObservableSet<TradeStatistics> observableTradeStatisticsSet = FXCollections.observableSet();
private final HashSet<TradeStatistics> tradeStatisticsSet = new HashSet<>();
@ -40,10 +42,12 @@ public class TradeStatisticsManager implements PersistedDataHost {
@Inject
public TradeStatisticsManager(Storage<TradeStatisticsList> statisticsStorage,
P2PService p2PService,
PriceFeedService priceFeedService,
@Named(Storage.STORAGE_DIR) File storageDir,
@Named(AppOptionKeys.DUMP_STATISTICS) boolean dumpStatistics) {
this.statisticsStorage = statisticsStorage;
this.p2PService = p2PService;
this.priceFeedService = priceFeedService;
this.dumpStatistics = dumpStatistics;
jsonFileManager = new JsonFileManager(storageDir);
@ -129,6 +133,8 @@ public class TradeStatisticsManager implements PersistedDataHost {
});
applyBisqMarketPrice();
statisticsStorage.queueUpForSave(new TradeStatisticsList(new ArrayList<>(tradeStatisticsSet)), 2000);
dump();
@ -137,6 +143,16 @@ public class TradeStatisticsManager implements PersistedDataHost {
}
private void applyBisqMarketPrice() {
List<TradeStatistics> sortedList = new ArrayList<>(tradeStatisticsSet);
// sort by date so we have most recent as last entry which we use for displaying the latest price
sortedList.sort((o1, o2) -> o1.getTradeDate().compareTo(o2.getTradeDate()));
if (!sortedList.isEmpty()) {
TradeStatistics tradeStatistics = sortedList.get(sortedList.size() - 1);
priceFeedService.setBisqMarketPrice(tradeStatistics.getCurrencyCode(), tradeStatistics.getTradePrice());
}
}
public void add(TradeStatistics tradeStatistics, boolean storeLocally) {
if (!tradeStatisticsSet.contains(tradeStatistics)) {
boolean itemAlreadyAdded = tradeStatisticsSet.stream().filter(e -> (e.getOfferId().equals(tradeStatistics.getOfferId()))).findAny().isPresent();
@ -144,7 +160,11 @@ public class TradeStatisticsManager implements PersistedDataHost {
tradeStatisticsSet.add(tradeStatistics);
observableTradeStatisticsSet.add(tradeStatistics);
tradeStatistics.getTradePrice().getValue();
if (storeLocally) {
applyBisqMarketPrice();
statisticsStorage.queueUpForSave(new TradeStatisticsList(new ArrayList<>(tradeStatisticsSet)), 2000);
dump();
}

View file

@ -159,7 +159,7 @@ public class MainView extends InitializableView<StackPane, MainViewModel> {
});
}, 1);
}
HBox leftNavPane = new HBox(marketButton, buyButton, sellButton, portfolioButtonHolder, fundsButton, disputesButtonHolder) {{
setLeftAnchor(this, 10d);
setTopAnchor(this, 0d);
@ -330,7 +330,7 @@ public class MainView extends InitializableView<StackPane, MainViewModel> {
HBox.setMargin(btcAverageIconButton, new Insets(0, 5, 0, 0));
btcAverageIconButton.setOnAction(e -> GUIUtil.openWebPage("https://bitcoinaverage.com"));
btcAverageIconButton.setVisible(model.isFiatCurrencyPriceFeedSelected.get());
btcAverageIconButton.setManaged(model.isFiatCurrencyPriceFeedSelected.get());
btcAverageIconButton.setManaged(btcAverageIconButton.isVisible());
btcAverageIconButton.visibleProperty().bind(model.isFiatCurrencyPriceFeedSelected);
btcAverageIconButton.managedProperty().bind(model.isFiatCurrencyPriceFeedSelected);
btcAverageIconButton.setOnMouseEntered(e -> {
@ -354,7 +354,7 @@ public class MainView extends InitializableView<StackPane, MainViewModel> {
HBox.setMargin(poloniexIconButton, new Insets(2, 3, 0, 0));
poloniexIconButton.setOnAction(e -> GUIUtil.openWebPage("https://poloniex.com"));
poloniexIconButton.setVisible(model.isCryptoCurrencyPriceFeedSelected.get());
poloniexIconButton.setManaged(model.isCryptoCurrencyPriceFeedSelected.get());
poloniexIconButton.setManaged(poloniexIconButton.isVisible());
poloniexIconButton.visibleProperty().bind(model.isCryptoCurrencyPriceFeedSelected);
poloniexIconButton.managedProperty().bind(model.isCryptoCurrencyPriceFeedSelected);
poloniexIconButton.setOnMouseEntered(e -> {
@ -372,10 +372,28 @@ public class MainView extends InitializableView<StackPane, MainViewModel> {
Label label = new Label(Res.get("mainView.marketPrice.provider"));
label.setId("nav-balance-label");
label.setPadding(new Insets(0, 5, 0, 2));
label.visibleProperty().bind(createBooleanBinding(() -> model.isCryptoCurrencyPriceFeedSelected.get() || model.isFiatCurrencyPriceFeedSelected.get(),
model.isCryptoCurrencyPriceFeedSelected, model.isFiatCurrencyPriceFeedSelected));
label.visibleProperty().bind(createBooleanBinding(() -> model.isPriceAvailable.get() &&
(model.isCryptoCurrencyPriceFeedSelected.get() ||
model.isFiatCurrencyPriceFeedSelected.get() ||
!model.isExternallyProvidedPrice.get()),
model.isPriceAvailable,
model.isCryptoCurrencyPriceFeedSelected,
model.isFiatCurrencyPriceFeedSelected,
model.isExternallyProvidedPrice));
label.managedProperty().bind(label.visibleProperty());
model.isExternallyProvidedPrice.addListener((observable, oldValue, newValue) -> {
if (newValue) {
label.setText(Res.get("mainView.marketPrice.provider"));
label.setTooltip(null);
} else {
label.setText(Res.get("mainView.marketPrice.bisqInternalPrice"));
final Tooltip tooltip = new Tooltip(Res.get("mainView.marketPrice.tooltip.bisqInternalPrice"));
tooltip.setStyle("-fx-font-size: 12");
label.setTooltip(tooltip);
}
});
HBox hBox2 = new HBox();
hBox2.getChildren().setAll(label, btcAverageIconButton, poloniexIconButton);

View file

@ -148,6 +148,8 @@ public class MainViewModel implements ViewModel {
final ObjectProperty<PriceFeedComboBoxItem> selectedPriceFeedComboBoxItemProperty = new SimpleObjectProperty<>();
final BooleanProperty isFiatCurrencyPriceFeedSelected = new SimpleBooleanProperty(true);
final BooleanProperty isCryptoCurrencyPriceFeedSelected = new SimpleBooleanProperty(false);
final BooleanProperty isExternallyProvidedPrice = new SimpleBooleanProperty(true);
final BooleanProperty isPriceAvailable = new SimpleBooleanProperty(false);
final StringProperty availableBalance = new SimpleStringProperty();
final StringProperty reservedBalance = new SimpleStringProperty();
final StringProperty lockedBalance = new SimpleStringProperty();
@ -1026,12 +1028,13 @@ public class MainViewModel implements ViewModel {
String currencyCode = item.currencyCode;
MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode);
String priceString;
if (marketPrice != null && marketPrice.isValid()) {
if (marketPrice != null && marketPrice.isPriceAvailable()) {
priceString = formatter.formatMarketPrice(marketPrice.getPrice(), currencyCode);
item.setIsPriceAvailable(true);
item.setPriceAvailable(true);
item.setExternallyProvidedPrice(marketPrice.isExternallyProvidedPrice());
} else {
priceString = Res.get("shared.na");
item.setIsPriceAvailable(false);
item.setPriceAvailable(false);
}
item.setDisplayString(formatter.getCurrencyPair(currencyCode) + ": " + priceString);
});
@ -1057,8 +1060,10 @@ public class MainViewModel implements ViewModel {
UserThread.runAfter(() -> {
if (item != null) {
String code = item.currencyCode;
isFiatCurrencyPriceFeedSelected.set(CurrencyUtil.isFiatCurrency(code) && CurrencyUtil.getFiatCurrency(code).isPresent() && item.isPriceAvailable());
isCryptoCurrencyPriceFeedSelected.set(CurrencyUtil.isCryptoCurrency(code) && CurrencyUtil.getCryptoCurrency(code).isPresent() && item.isPriceAvailable());
isFiatCurrencyPriceFeedSelected.set(CurrencyUtil.isFiatCurrency(code) && CurrencyUtil.getFiatCurrency(code).isPresent() && item.isPriceAvailable() && item.isExternallyProvidedPrice());
isCryptoCurrencyPriceFeedSelected.set(CurrencyUtil.isCryptoCurrency(code) && CurrencyUtil.getCryptoCurrency(code).isPresent() && item.isPriceAvailable() && item.isExternallyProvidedPrice());
isExternallyProvidedPrice.set(item.isExternallyProvidedPrice());
isPriceAvailable.set(item.isPriceAvailable());
}
}, 100, TimeUnit.MILLISECONDS);
}

View file

@ -2,15 +2,18 @@ package io.bisq.gui.main;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import lombok.Getter;
import lombok.Setter;
public class PriceFeedComboBoxItem {
private static final Logger log = LoggerFactory.getLogger(PriceFeedComboBoxItem.class);
public final String currencyCode;
public final StringProperty displayStringProperty = new SimpleStringProperty();
@Setter
@Getter
private boolean isPriceAvailable;
@Setter
@Getter
private boolean isExternallyProvidedPrice;
public PriceFeedComboBoxItem(String currencyCode) {
this.currencyCode = currencyCode;
@ -19,12 +22,4 @@ public class PriceFeedComboBoxItem {
public void setDisplayString(String displayString) {
this.displayStringProperty.set(displayString);
}
public void setIsPriceAvailable(boolean isPriceAvailable) {
this.isPriceAvailable = isPriceAvailable;
}
public boolean isPriceAvailable() {
return isPriceAvailable;
}
}

View file

@ -146,7 +146,7 @@ class SpreadViewModel extends ActivatableViewModel {
// TODO maybe show extra colums with spread and use real amount diff
// not % based. e.g. diff between best buy and sell offer (of small amounts its a smaller gain)
if (spread != null && marketPrice != null && marketPrice.isValid()) {
if (spread != null && marketPrice != null && marketPrice.isPriceAvailable()) {
double marketPriceAsDouble = marketPrice.getPrice();
final double precision = isFiatCurrency ?
Math.pow(10, Fiat.SMALLEST_UNIT_EXPONENT) :

View file

@ -301,7 +301,7 @@ class CreateOfferViewModel extends ActivatableWithDataModel<CreateOfferDataModel
dataModel.calculateTotalToPay();
if (!inputIsMarketBasedPrice) {
if (marketPrice != null && marketPrice.isValid()) {
if (marketPrice != null && marketPrice.isRecentExternalPriceAvailable()) {
double marketPriceAsDouble = marketPrice.getPrice();
try {
double priceAsDouble = btcFormatter.parseNumberStringToDouble(price.get());
@ -338,7 +338,7 @@ class CreateOfferViewModel extends ActivatableWithDataModel<CreateOfferDataModel
} else {
final String currencyCode = dataModel.getTradeCurrencyCode().get();
MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode);
if (marketPrice != null && marketPrice.isValid()) {
if (marketPrice != null && marketPrice.isRecentExternalPriceAvailable()) {
percentage = MathUtils.roundDouble(percentage, 4);
double marketPriceAsDouble = marketPrice.getPrice();
final boolean isCryptoCurrency = CurrencyUtil.isCryptoCurrency(currencyCode);
@ -462,8 +462,8 @@ class CreateOfferViewModel extends ActivatableWithDataModel<CreateOfferDataModel
private void updateMarketPriceAvailable() {
marketPrice = priceFeedService.getMarketPrice(dataModel.getTradeCurrencyCode().get());
marketPriceAvailableProperty.set(marketPrice == null ? 0 : 1);
dataModel.setMarketPriceAvailable(marketPrice != null);
marketPriceAvailableProperty.set(marketPrice == null || !marketPrice.isExternallyProvidedPrice() ? 0 : 1);
dataModel.setMarketPriceAvailable(marketPrice != null && marketPrice.isExternallyProvidedPrice());
}
private void addListeners() {
@ -603,7 +603,7 @@ class CreateOfferViewModel extends ActivatableWithDataModel<CreateOfferDataModel
dataModel.onCurrencySelected(tradeCurrency);
marketPrice = priceFeedService.getMarketPrice(dataModel.getTradeCurrencyCode().get());
marketPriceAvailableProperty.set(marketPrice == null ? 0 : 1);
marketPriceAvailableProperty.set(marketPrice == null || !marketPrice.isExternallyProvidedPrice() ? 0 : 1);
updateButtonDisableState();
}

View file

@ -45,7 +45,7 @@ public class BsqValidator extends AltcoinValidator {
@Inject
public BsqValidator(BsqFormatter bsqFormatter) {
this.bsqFormatter = bsqFormatter;
setMaxValue(bsqFormatter.parseToCoin("2300000")); // TODO make it lower
//setMaxValue(bsqFormatter.parseToCoin("2500000"));
}
public void setMaxValue(@NotNull Coin maxValue) {