All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.xeiam.xchange.ripple.service.polling.RippleTradeServiceRaw Maven / Gradle / Ivy

The newest version!
package com.xeiam.xchange.ripple.service.polling;

import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.xeiam.xchange.Exchange;
import com.xeiam.xchange.currency.Currencies;
import com.xeiam.xchange.currency.CurrencyPair;
import com.xeiam.xchange.dto.Order.OrderType;
import com.xeiam.xchange.exceptions.ExchangeException;
import com.xeiam.xchange.ripple.RippleExchange;
import com.xeiam.xchange.ripple.dto.RippleAmount;
import com.xeiam.xchange.ripple.dto.RippleException;
import com.xeiam.xchange.ripple.dto.account.ITransferFeeSource;
import com.xeiam.xchange.ripple.dto.trade.RippleAccountOrders;
import com.xeiam.xchange.ripple.dto.trade.RippleLimitOrder;
import com.xeiam.xchange.ripple.dto.trade.RippleNotifications;
import com.xeiam.xchange.ripple.dto.trade.RippleNotifications.RippleNotification;
import com.xeiam.xchange.ripple.dto.trade.RippleOrderCancelRequest;
import com.xeiam.xchange.ripple.dto.trade.RippleOrderCancelResponse;
import com.xeiam.xchange.ripple.dto.trade.RippleOrderDetails;
import com.xeiam.xchange.ripple.dto.trade.RippleOrderEntryRequest;
import com.xeiam.xchange.ripple.dto.trade.RippleOrderEntryRequestBody;
import com.xeiam.xchange.ripple.dto.trade.RippleOrderEntryResponse;
import com.xeiam.xchange.ripple.service.polling.params.RippleTradeHistoryCount;
import com.xeiam.xchange.ripple.service.polling.params.RippleTradeHistoryHashLimit;
import com.xeiam.xchange.service.polling.trade.params.TradeHistoryParamCurrencyPair;
import com.xeiam.xchange.service.polling.trade.params.TradeHistoryParamPaging;
import com.xeiam.xchange.service.polling.trade.params.TradeHistoryParams;
import com.xeiam.xchange.service.polling.trade.params.TradeHistoryParamsTimeSpan;

public class RippleTradeServiceRaw extends RippleBasePollingService {

  private static final Boolean EXCLUDE_FAILED = true;
  private static final Boolean EARLIEST_FIRST = false;
  private static final Long START_LEDGER = null;
  private static final Long END_LEDGER = null;

  private final Logger logger = LoggerFactory.getLogger(getClass());

  private final Map> orderDetailsStore = new ConcurrentHashMap>();

  public RippleTradeServiceRaw(final Exchange exchange) {
    super(exchange);
  }

  public String placeOrder(final RippleLimitOrder order, final boolean validate) throws RippleException, IOException {
    final RippleOrderEntryRequest entry = new RippleOrderEntryRequest();
    entry.setSecret(exchange.getExchangeSpecification().getSecretKey());

    final RippleOrderEntryRequestBody request = entry.getOrder();

    final RippleAmount baseAmount;
    final RippleAmount counterAmount;
    if (order.getType() == OrderType.BID) {
      request.setType("buy");
      // buying: we receive base and pay with counter, taker receives counter and pays with base
      counterAmount = request.getTakerGets();
      baseAmount = request.getTakerPays();
    } else {
      request.setType("sell");
      // selling: we receive counter and pay with base, taker receives base and pays with counter
      baseAmount = request.getTakerGets();
      counterAmount = request.getTakerPays();
    }

    baseAmount.setCurrency(order.getCurrencyPair().baseSymbol);
    baseAmount.setValue(order.getTradableAmount());
    if (baseAmount.getCurrency().equals(Currencies.XRP) == false) {
      // not XRP - need a counterparty for this currency
      final String counterparty = order.getBaseCounterparty();
      if (counterparty.isEmpty()) {
        throw new ExchangeException(String.format("base counterparty must be populated for currency %s", baseAmount.getCurrency()));
      }
      baseAmount.setCounterparty(counterparty.toString());
    }

    counterAmount.setCurrency(order.getCurrencyPair().counterSymbol);
    counterAmount.setValue(order.getTradableAmount().multiply(order.getLimitPrice()));
    if (counterAmount.getCurrency().equals(Currencies.XRP) == false) {
      // not XRP - need a counterparty for this currency
      final String counterparty = order.getCounterCounterparty();
      if (counterparty.isEmpty()) {
        throw new ExchangeException(String.format("counter counterparty must be populated for currency %s", counterAmount.getCurrency()));
      }
      counterAmount.setCounterparty(counterparty.toString());
    }

    final RippleOrderEntryResponse response = rippleAuthenticated.orderEntry(exchange.getExchangeSpecification().getApiKey(), validate, entry);
    return Long.toString(response.getOrder().getSequence());
  }

  public boolean cancelOrder(final String orderId, final boolean validate) throws RippleException, IOException {
    final RippleOrderCancelRequest cancel = new RippleOrderCancelRequest();
    cancel.setSecret(exchange.getExchangeSpecification().getSecretKey());

    final RippleOrderCancelResponse response = rippleAuthenticated.orderCancel(exchange.getExchangeSpecification().getApiKey(), Long.valueOf(orderId),
        validate, cancel);
    return response.isSuccess();
  }

  public RippleAccountOrders getOpenAccountOrders() throws RippleException, IOException {
    return ripplePublic.openAccountOrders(exchange.getExchangeSpecification().getApiKey());
  }

  public RippleNotifications getNotifications(final String account, final Boolean excludeFailed, final Boolean earliestFirst,
      final Integer resultsPerPage, final Integer page, final Long startLedger, final Long endLedger) throws RippleException, IOException {
    return ripplePublic.notifications(account, excludeFailed, earliestFirst, resultsPerPage, page, startLedger, endLedger);
  }

  /**
   * Retrieve order details from local store if they have been previously stored otherwise query external server.
   */
  public RippleOrderDetails getOrderDetails(final String account, final String hash) throws RippleException, IOException {
    final RippleExchange ripple = (RippleExchange) exchange;
    if (ripple.isStoreOrderDetails()) {
      Map cache = orderDetailsStore.get(account);
      if (cache == null) {
        cache = new ConcurrentHashMap();
        orderDetailsStore.put(account, cache);
      }
      if (cache.containsKey(hash)) {
        return cache.get(hash);
      }
    }

    final RippleOrderDetails orderDetails;
    try {
      orderDetails = ripplePublic.orderDetails(account, hash);
    } catch (final RippleException e) {
      if (e.getHttpStatusCode() == 500 && e.getErrorType().equals("transaction")) {
        // Do not let an individual transaction parsing bug in the Ripple REST service cause a total trade   
        // history failure. See https://github.com/ripple/ripple-rest/issues/384 as an example of this situation. 
        logger.error("exception reading order transaction[{}] for account[{}]", hash, account, e);
        return null;
      } else {
        throw e;
      }
    }
    if (ripple.isStoreOrderDetails()) {
      orderDetailsStore.get(account).put(hash, orderDetails);
    }
    return orderDetails;
  }

  public List getTradesForAccount(final TradeHistoryParams params, final String account) throws RippleException, IOException {
    final Integer pageLength;
    final Integer pageNumber;
    if (params instanceof TradeHistoryParamPaging) {
      final TradeHistoryParamPaging pagingParams = (TradeHistoryParamPaging) params;
      pageLength = pagingParams.getPageLength();
      pageNumber = pagingParams.getPageNumber();
    } else {
      pageLength = pageNumber = null;
    }

    final Collection currencyFilter = new HashSet();
    if (params instanceof TradeHistoryParamCurrencyPair) {
      final CurrencyPair pair = ((TradeHistoryParamCurrencyPair) params).getCurrencyPair();
      if (pair != null) {
        currencyFilter.add(pair.baseSymbol);
        currencyFilter.add(pair.counterSymbol);
      }
    }

    final Date startTime, endTime;
    if (params instanceof TradeHistoryParamsTimeSpan) {
      final TradeHistoryParamsTimeSpan timeSpanParams = (TradeHistoryParamsTimeSpan) params;
      // return all trades between start time (oldest) and end time (most recent)
      startTime = timeSpanParams.getStartTime();
      endTime = timeSpanParams.getEndTime();
    } else {
      startTime = endTime = null;
    }

    final RippleTradeHistoryCount rippleCount;
    if (params instanceof RippleTradeHistoryCount) {
      rippleCount = (RippleTradeHistoryCount) params;
    } else {
      rippleCount = null;
    }

    final String hashLimit;
    if (params instanceof RippleTradeHistoryHashLimit) {
      hashLimit = ((RippleTradeHistoryHashLimit) params).getHashLimit();
    } else {
      hashLimit = null;
    }

    final List trades = new ArrayList();

    final RippleNotifications notifications = ripplePublic.notifications(account, EXCLUDE_FAILED, EARLIEST_FIRST, pageLength, pageNumber,
        START_LEDGER, END_LEDGER);
    if (rippleCount != null) {
      rippleCount.incrementApiCallCount();
    }
    if (notifications.getNotifications().isEmpty()) {
      return trades;
    }

    // Notifications are returned with the most recent at bottom of the result page. Therefore,
    // in order to consider the most recent first, loop through using a reverse order iterator.
    final ListIterator iterator = notifications.getNotifications().listIterator(notifications.getNotifications().size());
    while (iterator.hasPrevious()) {
      if (rippleCount != null) {
        if (rippleCount.getTradeCountLimit() > 0 && rippleCount.getTradeCount() >= rippleCount.getTradeCountLimit()) {
          return trades; // found enough trades
        }
        if (rippleCount.getApiCallCountLimit() > 0 && rippleCount.getApiCallCount() >= rippleCount.getApiCallCountLimit()) {
          return trades; // reached the query limit
        }
      }

      final RippleNotification notification = iterator.previous();
      if ((endTime != null) && notification.getTimestamp().after(endTime)) {
        // this trade is more recent than the end time - ignore it
        continue;
      }
      if ((startTime != null) && notification.getTimestamp().before(startTime)) {
        // this trade is older than the start time - stop searching
        return trades;
      }

      if (notification.getType().equals("order")) {
        final RippleOrderDetails orderDetails = getOrderDetails(account, notification.getHash());
        if (rippleCount != null) {
          rippleCount.incrementApiCallCount();
        }
        if (orderDetails == null) {
          continue;
        }

        final List balanceChanges = orderDetails.getBalanceChanges();
        if (balanceChanges.size() != 2) {
          continue; // this is not a trade - a trade will change 2 currency balances
        }

        if (currencyFilter.isEmpty()
            || (currencyFilter.contains(balanceChanges.get(0).getCurrency()) && currencyFilter.contains(balanceChanges.get(1).getCurrency()))) {
          // no currency filter has been applied || currency filter match
          trades.add(orderDetails);
          if (rippleCount != null) {
            rippleCount.incrementTradeCount();
          }
        }

        if (orderDetails.getHash().equals(hashLimit)) {
          return trades; // found the last required trade - stop searching
        }
      }
    }

    if ((params instanceof TradeHistoryParamPaging) && ((hashLimit != null) || (startTime != null))) {
      // Still looking for trades, if query was complete it would have returned in the
      // loop above. Increment the page number and search next set of notifications.
      final TradeHistoryParamPaging pagingParams = (TradeHistoryParamPaging) params;
      final int currentPage;
      if (pagingParams.getPageNumber() == null) {
        currentPage = 1;
      } else {
        currentPage = pagingParams.getPageNumber();
      }
      pagingParams.setPageNumber(currentPage + 1);
      trades.addAll(getTradesForAccount(params, account));
    }

    return trades;
  }

  /**
   * The Ripple network transaction fee varies depending on how busy the network is as described
   * here.
   *
   * @return current network transaction fee in units of XRP
   */
  public BigDecimal getTransactionFee() {
    return ripplePublic.getTransactionFee().getFee().stripTrailingZeros();
  }

  /**
   * @return transfer fee for the base leg of the order in the base currency
   */
  public BigDecimal getExpectedBaseTransferFee(final RippleLimitOrder order) throws IOException {
    final ITransferFeeSource transferFeeSource = (ITransferFeeSource) exchange.getPollingAccountService();
    final String counterparty = order.getBaseCounterparty();
    final String currency = order.getCurrencyPair().baseSymbol;
    final BigDecimal quantity = order.getTradableAmount();
    final OrderType type = order.getType();
    return getExpectedTransferFee(transferFeeSource, counterparty, currency, quantity, type);
  }

  /**
   * @return transfer fee for the counter leg of the order in the counter currency
   */
  public BigDecimal getExpectedCounterTransferFee(final RippleLimitOrder order) throws IOException {
    final ITransferFeeSource transferFeeSource = (ITransferFeeSource) exchange.getPollingAccountService();
    final String counterparty = order.getCounterCounterparty();
    final String currency = order.getCurrencyPair().counterSymbol;
    final BigDecimal quantity = order.getTradableAmount().multiply(order.getLimitPrice());
    final OrderType type;
    if (order.getType() == OrderType.BID) {
      type = OrderType.ASK;
    } else {
      type = OrderType.BID;
    }
    return getExpectedTransferFee(transferFeeSource, counterparty, currency, quantity, type);
  }

  /**
   * The expected counterparty transfer fee for an order that results in a transfer of the supplied amount of currency. The fee rate is payable when
   * sending the currency (not receiving it) and it set by the issuing counterparty. The rate may be zero. Transfer fees are not applicable to sending
   * XRP. More details can be found here.
   *
   * @return transfer fee of the supplied currency
   */
  public static BigDecimal getExpectedTransferFee(final ITransferFeeSource transferFeeSource, final String counterparty, final String currency,
      final BigDecimal quantity, final OrderType type) throws IOException {
    if (currency.equals(Currencies.XRP)) {
      return BigDecimal.ZERO;
    }
    if (counterparty.isEmpty()) {
      return BigDecimal.ZERO;
    }
    final BigDecimal transferFeeRate = transferFeeSource.getTransferFeeRate(counterparty);
    if ((transferFeeRate.compareTo(BigDecimal.ZERO) > 0) && (type == OrderType.ASK)) {
      // selling/sending the base currency so will incur a transaction fee on it
      return quantity.multiply(transferFeeRate).abs();
    } else {
      return BigDecimal.ZERO;
    }
  }

  /**
   * Clear any stored order details to allow memory to be released.
   */
  public void clearOrderDetailsStore() {
    for (final Map cache : orderDetailsStore.values()) {
      cache.clear();
    }
    orderDetailsStore.clear();
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy