package org.knowm.xchange.ripple.service;
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.knowm.xchange.Exchange;
import org.knowm.xchange.currency.CurrencyPair;
import org.knowm.xchange.dto.Order.OrderType;
import org.knowm.xchange.exceptions.ExchangeException;
import org.knowm.xchange.ripple.RippleExchange;
import org.knowm.xchange.ripple.dto.RippleAmount;
import org.knowm.xchange.ripple.dto.RippleException;
import org.knowm.xchange.ripple.dto.account.ITransferFeeSource;
import org.knowm.xchange.ripple.dto.trade.IRippleTradeTransaction;
import org.knowm.xchange.ripple.dto.trade.RippleAccountOrders;
import org.knowm.xchange.ripple.dto.trade.RippleLimitOrder;
import org.knowm.xchange.ripple.dto.trade.RippleNotifications;
import org.knowm.xchange.ripple.dto.trade.RippleNotifications.RippleNotification;
import org.knowm.xchange.ripple.dto.trade.RippleOrderCancelRequest;
import org.knowm.xchange.ripple.dto.trade.RippleOrderCancelResponse;
import org.knowm.xchange.ripple.dto.trade.RippleOrderEntryRequest;
import org.knowm.xchange.ripple.dto.trade.RippleOrderEntryRequestBody;
import org.knowm.xchange.ripple.dto.trade.RippleOrderEntryResponse;
import org.knowm.xchange.ripple.service.params.RippleTradeHistoryCount;
import org.knowm.xchange.ripple.service.params.RippleTradeHistoryHashLimit;
import org.knowm.xchange.service.trade.params.TradeHistoryParamCurrencyPair;
import org.knowm.xchange.service.trade.params.TradeHistoryParamPaging;
import org.knowm.xchange.service.trade.params.TradeHistoryParams;
import org.knowm.xchange.service.trade.params.TradeHistoryParamsTimeSpan;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class RippleTradeServiceRaw extends RippleBaseService {
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<String, Map<String, IRippleTradeTransaction>> rawTradeStore = new ConcurrentHashMap<String, Map<String, IRippleTradeTransaction>>();
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().base.getCurrencyCode());
baseAmount.setValue(order.getTradableAmount());
if (baseAmount.getCurrency().equals("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().counter.getCurrencyCode());
counterAmount.setValue(order.getTradableAmount().multiply(order.getLimitPrice()));
if (counterAmount.getCurrency().equals("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 IRippleTradeTransaction getTrade(final String account, final RippleNotification notification) throws RippleException, IOException {
final RippleExchange ripple = (RippleExchange) exchange;
if (ripple.isStoreTradeTransactionDetails()) {
Map<String, IRippleTradeTransaction> cache = rawTradeStore.get(account);
if (cache == null) {
cache = new ConcurrentHashMap<String, IRippleTradeTransaction>();
rawTradeStore.put(account, cache);
}
if (cache.containsKey(notification.getHash())) {
return cache.get(notification.getHash());
}
}
final IRippleTradeTransaction trade;
try {
if (notification.getType().equals("order")) {
trade = ripplePublic.orderTransaction(account, notification.getHash());
} else if (notification.getType().equals("payment")) {
trade = ripplePublic.paymentTransaction(account, notification.getHash());
} else {
throw new IllegalArgumentException(String.format("unexpected notification %s type for transaction[%s] and account[%s]",
notification.getType(), notification.getHash(), notification.getAccount()));
}
} 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 {} transaction[{}] for account[{}]", notification.getType(), notification.getHash(), account, e);
return null;
} else {
throw e;
}
}
if (ripple.isStoreTradeTransactionDetails()) {
rawTradeStore.get(account).put(notification.getHash(), trade);
}
return trade;
}
public List<IRippleTradeTransaction> 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<String> currencyFilter = new HashSet<String>();
if (params instanceof TradeHistoryParamCurrencyPair) {
final CurrencyPair pair = ((TradeHistoryParamCurrencyPair) params).getCurrencyPair();
if (pair != null) {
currencyFilter.add(pair.base.getCurrencyCode());
currencyFilter.add(pair.counter.getCurrencyCode());
}
}
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<IRippleTradeTransaction> trades = new ArrayList<IRippleTradeTransaction>();
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<RippleNotification> 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")) {
// standard on single order book trade
} else if (notification.getType().equals("payment") && notification.getDirection().equals("passthrough")) {
// indirect trade passing through this order book
} else {
continue; // not a trade related notification
}
final IRippleTradeTransaction trade = getTrade(account, notification);
if (rippleCount != null) {
rippleCount.incrementApiCallCount();
}
if (trade == null) {
continue;
}
final List<RippleAmount> balanceChanges = trade.getBalanceChanges();
if (balanceChanges.size() < 2 || balanceChanges.size() > 3) {
continue; // this is not a trade - a trade will change 2 or 3 (including XRP fee) 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(trade);
if (rippleCount != null) {
rippleCount.incrementTradeCount();
}
}
if (trade.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
* <a href="https://wiki.ripple.com/Transaction_Fee">here</a>.
*
* @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.getAccountService();
final String counterparty = order.getBaseCounterparty();
final String currency = order.getCurrencyPair().base.getCurrencyCode();
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.getAccountService();
final String counterparty = order.getCounterCounterparty();
final String currency = order.getCurrencyPair().counter.getCurrencyCode();
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 <a href="https://wiki.ripple.com/Transit_Fee">here</a>.
*
* @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("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<String, IRippleTradeTransaction> cache : rawTradeStore.values()) {
cache.clear();
}
rawTradeStore.clear();
}
}