
com.xeiam.xchange.ripple.RippleAdapters Maven / Gradle / Ivy
Show all versions of xchange-ripple Show documentation
package com.xeiam.xchange.ripple;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import com.xeiam.xchange.currency.Currencies;
import com.xeiam.xchange.currency.CurrencyPair;
import com.xeiam.xchange.dto.Order.OrderType;
import com.xeiam.xchange.dto.account.AccountInfo;
import com.xeiam.xchange.dto.marketdata.OrderBook;
import com.xeiam.xchange.dto.marketdata.Trades.TradeSortType;
import com.xeiam.xchange.dto.trade.LimitOrder;
import com.xeiam.xchange.dto.trade.OpenOrders;
import com.xeiam.xchange.dto.trade.UserTrade;
import com.xeiam.xchange.dto.trade.UserTrades;
import com.xeiam.xchange.dto.trade.Wallet;
import com.xeiam.xchange.ripple.dto.RippleAmount;
import com.xeiam.xchange.ripple.dto.account.ITransferFeeSource;
import com.xeiam.xchange.ripple.dto.account.RippleAccountBalances;
import com.xeiam.xchange.ripple.dto.account.RippleBalance;
import com.xeiam.xchange.ripple.dto.marketdata.RippleOrder;
import com.xeiam.xchange.ripple.dto.marketdata.RippleOrderBook;
import com.xeiam.xchange.ripple.dto.trade.RippleAccountOrders;
import com.xeiam.xchange.ripple.dto.trade.RippleAccountOrdersBody;
import com.xeiam.xchange.ripple.dto.trade.RippleLimitOrder;
import com.xeiam.xchange.ripple.dto.trade.RippleOrderDetails;
import com.xeiam.xchange.ripple.dto.trade.RippleUserTrade;
import com.xeiam.xchange.ripple.service.polling.RippleAccountService;
import com.xeiam.xchange.ripple.service.polling.RippleTradeServiceRaw;
import com.xeiam.xchange.ripple.service.polling.params.RippleMarketDataParams;
import com.xeiam.xchange.ripple.service.polling.params.RippleTradeHistoryPreferredCurrencies;
import com.xeiam.xchange.service.polling.trade.params.TradeHistoryParamCurrencyPair;
import com.xeiam.xchange.service.polling.trade.params.TradeHistoryParams;
import com.xeiam.xchange.utils.jackson.CurrencyPairDeserializer;
/**
* Various adapters for converting from Ripple DTOs to XChange DTOs
*/
public abstract class RippleAdapters {
private static final Set EMPTY_STRING_SET = Collections.unmodifiableSet(new HashSet());
/**
* private Constructor
*/
private RippleAdapters() {
}
/**
* Adapts a Ripple Account to an XChange AccountInfo object.
*
* Counterparties are added to symbol since there is no other way of the application receiving this information.
*/
public static AccountInfo adaptAccountInfo(final RippleAccountBalances account, final String username) {
// Adapt account balances to XChange wallets
final Map wallets = new TreeMap();
for (final RippleBalance balance : account.getBalances()) {
final String currency;
if (balance.getCurrency().equals(Currencies.XRP)) {
currency = balance.getCurrency();
} else {
currency = String.format("%s.%s", balance.getCurrency(), balance.getCounterparty());
}
final Wallet wallet = new Wallet(currency, balance.getValue());
wallets.put(wallet.getCurrency(), wallet);
}
return new AccountInfo(username, BigDecimal.ZERO, wallets);
}
/**
* Adapts a Ripple OrderBook to an XChange OrderBook object.
*
* Counterparties are not mapped since the application calling this should know and keep track of the counterparties it is using in the polling
* thread.
*/
public static OrderBook adaptOrderBook(final RippleOrderBook rippleOrderBook, final RippleMarketDataParams params,
final CurrencyPair currencyPair) {
final String orderBook = rippleOrderBook.getOrderBook(); // e.g. XRP/BTC+rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B
final String[] splitPair = orderBook.split("/");
final String[] baseSplit = splitPair[0].split("\\+");
final String baseSymbol = baseSplit[0];
if (baseSymbol.equals(currencyPair.baseSymbol) == false) {
throw new IllegalStateException(String.format("base symbol in Ripple order book %s does not match requested base %s", orderBook, currencyPair));
}
final String baseCounterparty;
if (baseSymbol.equals(Currencies.XRP)) {
baseCounterparty = ""; // native currency
} else {
baseCounterparty = baseSplit[1];
}
if (baseCounterparty.equals(params.getBaseCounterparty()) == false) {
throw new IllegalStateException(String.format("base counterparty in Ripple order book %s does not match requested counterparty %s", orderBook,
params.getBaseCounterparty()));
}
final String[] counterSplit = splitPair[1].split("\\+");
final String counterSymbol = counterSplit[0];
if (counterSymbol.equals(currencyPair.counterSymbol) == false) {
throw new IllegalStateException(
String.format("counter symbol in Ripple order book %s does not match requested base %s", orderBook, currencyPair));
}
final String counterCounterparty;
if (counterSymbol.equals(Currencies.XRP)) {
counterCounterparty = ""; // native currency
} else {
counterCounterparty = counterSplit[1];
}
if (counterCounterparty.equals(params.getCounterCounterparty()) == false) {
throw new IllegalStateException(String.format("counter counterparty in Ripple order book %s does not match requested counterparty %s",
orderBook, params.getCounterCounterparty()));
}
final List bids = createOrders(currencyPair, OrderType.BID, rippleOrderBook.getBids(), baseCounterparty, counterCounterparty);
final List asks = createOrders(currencyPair, OrderType.ASK, rippleOrderBook.getAsks(), baseCounterparty, counterCounterparty);
return new OrderBook(null, asks, bids);
}
public static List createOrders(final CurrencyPair currencyPair, final OrderType orderType, final List orders,
final String baseCounterparty, final String counterCounterparty) {
final List limitOrders = new ArrayList();
for (final RippleOrder rippleOrder : orders) {
// Taker Pays = the amount the taker must pay to consume this order.
// Taker Gets = the amount the taker will get once the order is consumed.
//
// Funded vs Unfunded https://wiki.ripple.com/Unfunded_offers
// amount of base currency
final BigDecimal tradableAmount;
if (orderType == OrderType.BID) {
tradableAmount = rippleOrder.getTakerPaysFunded().getValue();
} else {
tradableAmount = rippleOrder.getTakerGetsFunded().getValue();
}
// price in counter currency
final BigDecimal price = rippleOrder.getPrice().getValue();
final RippleLimitOrder order = new RippleLimitOrder(orderType, tradableAmount, currencyPair, Integer.toString(rippleOrder.getSequence()), null,
price, baseCounterparty, counterCounterparty);
limitOrders.add(order);
}
return limitOrders;
}
/**
* Adapts a Ripple Account Orders object to an XChange OpenOrders object
*
* Counterparties set in additional data since there is no other way of the application receiving this information.
*/
public static OpenOrders adaptOpenOrders(final RippleAccountOrders rippleOrders, final int scale) {
final List list = new ArrayList(rippleOrders.getOrders().size());
for (final RippleAccountOrdersBody order : rippleOrders.getOrders()) {
final OrderType orderType;
final RippleAmount baseAmount;
final RippleAmount counterAmount;
if (order.getType().equals("buy")) {
// buying: we receive base and pay with counter, taker receives counter and pays with base
counterAmount = order.getTakerGets();
baseAmount = order.getTakerPays();
orderType = OrderType.BID;
} else {
// selling: we receive counter and pay with base, taker receives base and pays with counter
baseAmount = order.getTakerGets();
counterAmount = order.getTakerPays();
orderType = OrderType.ASK;
}
final String baseSymbol = baseAmount.getCurrency();
final String counterSymbol = counterAmount.getCurrency();
// need to provide rounding scale to prevent ArithmeticException
final BigDecimal price = counterAmount.getValue().divide(baseAmount.getValue(), scale, RoundingMode.HALF_UP).stripTrailingZeros();
final CurrencyPair pair = new CurrencyPair(baseSymbol, counterSymbol);
final RippleLimitOrder xchangeOrder = (RippleLimitOrder) new RippleLimitOrder.Builder(orderType, pair)
.baseCounterparty(baseAmount.getCounterparty()).counterCounterparty(counterAmount.getCounterparty()).id(Long.toString(order.getSequence()))
.limitPrice(price).timestamp(null).tradableAmount(baseAmount.getValue()).build();
list.add(xchangeOrder);
}
return new OpenOrders(list);
}
public static UserTrade adaptTrade(final RippleOrderDetails info, final TradeHistoryParams params, final ITransferFeeSource transferFeeSource,
final int scale) throws IOException {
// The order{} section of the body cannot be used to determine trade facts e.g. if the order was to sell BTC.Bitstamp and buy
// BTC.SnapSwap, and traded via XRP, and our trade was one of the XRP legs, all we'd see would be the taker getting and paying BTC.
//
// Details in the balance_changes{} and order_changes{} blocks are relative to the perspective account, i.e. the Ripple account address used in the URI.
final List balanceChanges = info.getBalanceChanges();
final Iterator iterator = balanceChanges.iterator();
while (iterator.hasNext()) {
final RippleAmount amount = iterator.next();
if (amount.getCurrency().equals(Currencies.XRP) && info.getFee().equals(amount.getValue().negate())) {
// XRP balance change is just the fee - it should not be part of the currency pair considerations
iterator.remove();
}
}
if (balanceChanges.size() != 2) {
throw new IllegalArgumentException("balance changes section should contains 2 currency amounts but found " + balanceChanges);
}
// There is no way of telling the original entered base or counter currency - Ripple just provides 2 currency adjustments.
// Check if TradeHistoryParams expressed a preference, otherwise arrange the currencies in the order they are supplied.
final Collection preferredBase, preferredCounter;
if (params instanceof RippleTradeHistoryPreferredCurrencies) {
final RippleTradeHistoryPreferredCurrencies rippleParams = (RippleTradeHistoryPreferredCurrencies) params;
preferredBase = rippleParams.getPreferredBaseCurrency();
preferredCounter = rippleParams.getPreferredCounterCurrency();
} else {
preferredBase = preferredCounter = EMPTY_STRING_SET;
}
final RippleAmount base, counter;
if (preferredBase.contains(balanceChanges.get(0).getCurrency())) {
base = balanceChanges.get(0);
counter = balanceChanges.get(1);
} else if (preferredBase.contains(balanceChanges.get(1).getCurrency())) {
base = balanceChanges.get(1);
counter = balanceChanges.get(0);
} else if (preferredCounter.contains(balanceChanges.get(0).getCurrency())) {
counter = balanceChanges.get(0);
base = balanceChanges.get(1);
} else if (preferredCounter.contains(balanceChanges.get(1).getCurrency())) {
counter = balanceChanges.get(1);
base = balanceChanges.get(0);
} else if ((params instanceof TradeHistoryParamCurrencyPair) && (((TradeHistoryParamCurrencyPair) params).getCurrencyPair() != null)) {
// Searching for a specific currency pair - use this direction
final CurrencyPair pair = ((TradeHistoryParamCurrencyPair) params).getCurrencyPair();
if (pair.baseSymbol.equals(balanceChanges.get(0).getCurrency()) && pair.counterSymbol.equals(balanceChanges.get(1).getCurrency())) {
base = balanceChanges.get(0);
counter = balanceChanges.get(1);
} else if (pair.baseSymbol.equals(balanceChanges.get(1).getCurrency()) && pair.counterSymbol.equals(balanceChanges.get(0).getCurrency())) {
base = balanceChanges.get(1);
counter = balanceChanges.get(0);
} else {
// Unexpected: this should have been filtered out in RippleTradeServiceRaw.getTradesForAccount(..) method.
throw new IllegalStateException(String.format("trade history param currency filter specified %s but trade query returned %s and %s", pair,
balanceChanges.get(0).getCurrency(), balanceChanges.get(1).getCurrency()));
}
} else { // select the currency direction as return from the API
base = balanceChanges.get(0);
counter = balanceChanges.get(1);
}
final OrderType type;
if (base.getValue().signum() == 1) {
type = OrderType.BID;
} else {
type = OrderType.ASK;
}
final String currencyPairString = base.getCurrency() + "/" + counter.getCurrency();
final CurrencyPair currencyPair = CurrencyPairDeserializer.getCurrencyPairFromString(currencyPairString);
// Ripple has 2 types of fee.
//
// (a) Transaction fee is a network charge levied in XRP.
// https://wiki.ripple.com/Transaction_Fee
//
// (b) Transfer fee charged by the issuer levied in the currency of traded instrument. Whoever
// sends an asset that has a transfer fee pays the fee, the receiver does not incur a charge.
// https://wiki.ripple.com/Transit_Fee
// https://ripple.com/knowledge_center/transfer-fees/
final BigDecimal baseTransferFee = RippleTradeServiceRaw.getExpectedTransferFee(transferFeeSource, base.getCounterparty(), base.getCurrency(),
base.getValue(), type);
base.setValue(base.getValue().abs().subtract(baseTransferFee));
final OrderType counterDirection;
if (type == OrderType.BID) {
counterDirection = OrderType.ASK;
} else {
counterDirection = OrderType.BID;
}
final BigDecimal counterTransferFee = RippleTradeServiceRaw.getExpectedTransferFee(transferFeeSource, counter.getCounterparty(),
counter.getCurrency(), counter.getValue(), counterDirection);
counter.setValue(counter.getValue().abs().subtract(counterTransferFee));
// Account for transaction fee in quantities
final BigDecimal transactionFee = info.getFee();
final BigDecimal quantity;
if (base.getCurrency().equals(Currencies.XRP)) {
if (type == OrderType.BID) {
quantity = base.getValue().add(transactionFee);
} else { // OrderType.ASK
quantity = base.getValue().subtract(transactionFee);
}
} else {
quantity = base.getValue();
}
final BigDecimal counterAmount;
if (counter.getCurrency().equals(Currencies.XRP)) {
if (type == OrderType.ASK) {
counterAmount = counter.getValue().add(transactionFee);
} else { // OrderType.BID
counterAmount = counter.getValue().subtract(transactionFee);
}
} else {
counterAmount = counter.getValue();
}
// need to provide rounding scale to prevent ArithmeticException
final BigDecimal price = counterAmount.divide(quantity, scale, RoundingMode.HALF_UP);
final String orderId = Long.toString(info.getOrder().getSequence());
final RippleUserTrade.Builder builder = (RippleUserTrade.Builder) new RippleUserTrade.Builder().currencyPair(currencyPair)
.feeAmount(transactionFee).feeCurrency(Currencies.XRP).id(info.getHash()).orderId(orderId).price(price.stripTrailingZeros())
.timestamp(info.getTimestamp()).tradableAmount(quantity.stripTrailingZeros()).type(type);
builder.baseTransferFee(baseTransferFee.abs());
builder.counterTransferFee(counterTransferFee.abs());
if (base.getCounterparty().length() > 0) {
builder.baseCounterparty(base.getCounterparty());
}
if (counter.getCounterparty().length() > 0) {
builder.counterCounterparty(counter.getCounterparty());
}
return builder.build();
}
public static UserTrades adaptTrades(final Collection tradesForAccount, final TradeHistoryParams params,
final RippleAccountService accountService, final int roundingScale) throws IOException {
final List trades = new ArrayList();
for (final RippleOrderDetails orderDetails : tradesForAccount) {
trades.add(adaptTrade(orderDetails, params, accountService, roundingScale));
}
return new UserTrades(trades, TradeSortType.SortByTimestamp);
}
}