PageRenderTime 48ms CodeModel.GetById 21ms RepoModel.GetById 1ms app.codeStats 0ms

/xchange-ripple/src/main/java/org/knowm/xchange/ripple/RippleAdapters.java

http://github.com/timmolter/XChange
Java | 455 lines | 358 code | 45 blank | 52 comment | 71 complexity | 8aad5645082825e3f11316f6198dceac MD5 | raw file
  1. package org.knowm.xchange.ripple;
  2. import java.io.IOException;
  3. import java.math.BigDecimal;
  4. import java.math.RoundingMode;
  5. import java.util.ArrayList;
  6. import java.util.Collection;
  7. import java.util.Collections;
  8. import java.util.HashMap;
  9. import java.util.Iterator;
  10. import java.util.LinkedList;
  11. import java.util.List;
  12. import java.util.Map;
  13. import java.util.Set;
  14. import org.knowm.xchange.currency.Currency;
  15. import org.knowm.xchange.currency.CurrencyPair;
  16. import org.knowm.xchange.dto.Order.OrderType;
  17. import org.knowm.xchange.dto.account.AccountInfo;
  18. import org.knowm.xchange.dto.account.Balance;
  19. import org.knowm.xchange.dto.account.Wallet;
  20. import org.knowm.xchange.dto.marketdata.OrderBook;
  21. import org.knowm.xchange.dto.marketdata.Trades.TradeSortType;
  22. import org.knowm.xchange.dto.trade.LimitOrder;
  23. import org.knowm.xchange.dto.trade.OpenOrders;
  24. import org.knowm.xchange.dto.trade.UserTrade;
  25. import org.knowm.xchange.dto.trade.UserTrades;
  26. import org.knowm.xchange.ripple.dto.RippleAmount;
  27. import org.knowm.xchange.ripple.dto.account.ITransferFeeSource;
  28. import org.knowm.xchange.ripple.dto.account.RippleAccountBalances;
  29. import org.knowm.xchange.ripple.dto.account.RippleBalance;
  30. import org.knowm.xchange.ripple.dto.marketdata.RippleOrder;
  31. import org.knowm.xchange.ripple.dto.marketdata.RippleOrderBook;
  32. import org.knowm.xchange.ripple.dto.trade.IRippleTradeTransaction;
  33. import org.knowm.xchange.ripple.dto.trade.RippleAccountOrders;
  34. import org.knowm.xchange.ripple.dto.trade.RippleAccountOrdersBody;
  35. import org.knowm.xchange.ripple.dto.trade.RippleLimitOrder;
  36. import org.knowm.xchange.ripple.dto.trade.RippleUserTrade;
  37. import org.knowm.xchange.ripple.service.RippleAccountService;
  38. import org.knowm.xchange.ripple.service.RippleTradeServiceRaw;
  39. import org.knowm.xchange.ripple.service.params.RippleMarketDataParams;
  40. import org.knowm.xchange.ripple.service.params.RippleTradeHistoryPreferredCurrencies;
  41. import org.knowm.xchange.service.trade.params.TradeHistoryParamCurrencyPair;
  42. import org.knowm.xchange.service.trade.params.TradeHistoryParams;
  43. import org.knowm.xchange.utils.jackson.CurrencyPairDeserializer;
  44. import org.slf4j.Logger;
  45. import org.slf4j.LoggerFactory;
  46. /** Various adapters for converting from Ripple DTOs to XChange DTOs */
  47. public abstract class RippleAdapters {
  48. private static final Set<Currency> EMPTY_CURRENCY_SET = Collections.emptySet();
  49. private static final Logger logger = LoggerFactory.getLogger(RippleAdapters.class);
  50. /** private Constructor */
  51. private RippleAdapters() {}
  52. /** Adapts a Ripple Account to an XChange Wallet object. */
  53. public static AccountInfo adaptAccountInfo(
  54. final RippleAccountBalances account, final String username) {
  55. // Adapt account balances to XChange balances
  56. final Map<String, List<Balance>> balances = new HashMap<>();
  57. for (final RippleBalance balance : account.getBalances()) {
  58. final String walletId;
  59. if (balance.getCurrency().equals("XRP")) {
  60. walletId = "main";
  61. } else {
  62. walletId = balance.getCounterparty();
  63. }
  64. if (!balances.containsKey(walletId)) {
  65. balances.put(walletId, new LinkedList<Balance>());
  66. }
  67. balances
  68. .get(walletId)
  69. .add(new Balance(Currency.getInstance(balance.getCurrency()), balance.getValue()));
  70. }
  71. final List<Wallet> accountInfo = new ArrayList<>(balances.size());
  72. for (final Map.Entry<String, List<Balance>> wallet : balances.entrySet()) {
  73. accountInfo.add(Wallet.Builder.from(wallet.getValue()).id(wallet.getKey()).build());
  74. }
  75. return new AccountInfo(username, BigDecimal.ZERO, accountInfo);
  76. }
  77. /**
  78. * Adapts a Ripple OrderBook to an XChange OrderBook object. Counterparties are not mapped since
  79. * the application calling this should know and keep track of the counterparties it is using in
  80. * the polling thread.
  81. */
  82. public static OrderBook adaptOrderBook(
  83. final RippleOrderBook rippleOrderBook,
  84. final RippleMarketDataParams params,
  85. final CurrencyPair currencyPair) {
  86. final String orderBook =
  87. rippleOrderBook.getOrderBook(); // e.g. XRP/BTC+rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B
  88. final String[] splitPair = orderBook.split("/");
  89. final String[] baseSplit = splitPair[0].split("\\+");
  90. final String baseSymbol = baseSplit[0];
  91. if (baseSymbol.equals(currencyPair.base.getCurrencyCode()) == false) {
  92. throw new IllegalStateException(
  93. String.format(
  94. "base symbol in Ripple order book %s does not match requested base %s",
  95. orderBook, currencyPair));
  96. }
  97. final String baseCounterparty;
  98. if (baseSymbol.equals("XRP")) {
  99. baseCounterparty = ""; // native currency
  100. } else {
  101. baseCounterparty = baseSplit[1];
  102. }
  103. if (baseCounterparty.equals(params.getBaseCounterparty()) == false) {
  104. throw new IllegalStateException(
  105. String.format(
  106. "base counterparty in Ripple order book %s does not match requested counterparty %s",
  107. orderBook, params.getBaseCounterparty()));
  108. }
  109. final String[] counterSplit = splitPair[1].split("\\+");
  110. final String counterSymbol = counterSplit[0];
  111. if (counterSymbol.equals(currencyPair.counter.getCurrencyCode()) == false) {
  112. throw new IllegalStateException(
  113. String.format(
  114. "counter symbol in Ripple order book %s does not match requested base %s",
  115. orderBook, currencyPair));
  116. }
  117. final String counterCounterparty;
  118. if (counterSymbol.equals("XRP")) {
  119. counterCounterparty = ""; // native currency
  120. } else {
  121. counterCounterparty = counterSplit[1];
  122. }
  123. if (counterCounterparty.equals(params.getCounterCounterparty()) == false) {
  124. throw new IllegalStateException(
  125. String.format(
  126. "counter counterparty in Ripple order book %s does not match requested counterparty %s",
  127. orderBook, params.getCounterCounterparty()));
  128. }
  129. final List<LimitOrder> bids =
  130. createOrders(
  131. currencyPair,
  132. OrderType.BID,
  133. rippleOrderBook.getBids(),
  134. baseCounterparty,
  135. counterCounterparty);
  136. final List<LimitOrder> asks =
  137. createOrders(
  138. currencyPair,
  139. OrderType.ASK,
  140. rippleOrderBook.getAsks(),
  141. baseCounterparty,
  142. counterCounterparty);
  143. return new OrderBook(null, asks, bids);
  144. }
  145. public static List<LimitOrder> createOrders(
  146. final CurrencyPair currencyPair,
  147. final OrderType orderType,
  148. final List<RippleOrder> orders,
  149. final String baseCounterparty,
  150. final String counterCounterparty) {
  151. final List<LimitOrder> limitOrders = new ArrayList<>();
  152. for (final RippleOrder rippleOrder : orders) {
  153. // Taker Pays = the amount the taker must pay to consume this order.
  154. // Taker Gets = the amount the taker will get once the order is consumed.
  155. //
  156. // Funded vs Unfunded https://wiki.ripple.com/Unfunded_offers
  157. // amount of base currency
  158. final BigDecimal originalAmount;
  159. if (orderType == OrderType.BID) {
  160. originalAmount = rippleOrder.getTakerPaysFunded().getValue();
  161. } else {
  162. originalAmount = rippleOrder.getTakerGetsFunded().getValue();
  163. }
  164. // price in counter currency
  165. final BigDecimal price = rippleOrder.getPrice().getValue();
  166. final RippleLimitOrder order =
  167. new RippleLimitOrder(
  168. orderType,
  169. originalAmount,
  170. currencyPair,
  171. Integer.toString(rippleOrder.getSequence()),
  172. null,
  173. price,
  174. baseCounterparty,
  175. counterCounterparty);
  176. limitOrders.add(order);
  177. }
  178. return limitOrders;
  179. }
  180. /**
  181. * Adapts a Ripple Account Orders object to an XChange OpenOrders object Counterparties set in
  182. * additional data since there is no other way of the application receiving this information.
  183. */
  184. public static OpenOrders adaptOpenOrders(
  185. final RippleAccountOrders rippleOrders, final int scale) {
  186. final List<LimitOrder> list = new ArrayList<>(rippleOrders.getOrders().size());
  187. for (final RippleAccountOrdersBody order : rippleOrders.getOrders()) {
  188. final OrderType orderType;
  189. final RippleAmount baseAmount;
  190. final RippleAmount counterAmount;
  191. if (order.getType().equals("buy")) {
  192. // buying: we receive base and pay with counter, taker receives counter and pays with base
  193. counterAmount = order.getTakerGets();
  194. baseAmount = order.getTakerPays();
  195. orderType = OrderType.BID;
  196. } else {
  197. // selling: we receive counter and pay with base, taker receives base and pays with counter
  198. baseAmount = order.getTakerGets();
  199. counterAmount = order.getTakerPays();
  200. orderType = OrderType.ASK;
  201. }
  202. final String baseSymbol = baseAmount.getCurrency();
  203. final String counterSymbol = counterAmount.getCurrency();
  204. // need to provide rounding scale to prevent ArithmeticException
  205. final BigDecimal price =
  206. counterAmount
  207. .getValue()
  208. .divide(baseAmount.getValue(), scale, RoundingMode.HALF_UP)
  209. .stripTrailingZeros();
  210. final CurrencyPair pair = new CurrencyPair(baseSymbol, counterSymbol);
  211. final RippleLimitOrder xchangeOrder =
  212. (RippleLimitOrder)
  213. new RippleLimitOrder.Builder(orderType, pair)
  214. .baseCounterparty(baseAmount.getCounterparty())
  215. .counterCounterparty(counterAmount.getCounterparty())
  216. .id(Long.toString(order.getSequence()))
  217. .limitPrice(price)
  218. .timestamp(null)
  219. .originalAmount(baseAmount.getValue())
  220. .build();
  221. list.add(xchangeOrder);
  222. }
  223. return new OpenOrders(list);
  224. }
  225. public static UserTrade adaptTrade(
  226. final IRippleTradeTransaction trade,
  227. final TradeHistoryParams params,
  228. final ITransferFeeSource transferFeeSource,
  229. final int scale)
  230. throws IOException {
  231. // The order{} section of the body cannot be used to determine trade facts e.g. if the order was
  232. // to sell BTC.Bitstamp and buy
  233. // BTC.SnapSwap, and traded via XRP, and our trade was one of the XRP legs, all we'd see would
  234. // be the taker getting and paying BTC.
  235. //
  236. // Details in the balance_changes{} and order_changes{} blocks are relative to the perspective
  237. // account, i.e. the Ripple account address used in the URI.
  238. final List<RippleAmount> balanceChanges = trade.getBalanceChanges();
  239. final Iterator<RippleAmount> iterator = balanceChanges.iterator();
  240. while (iterator.hasNext()) {
  241. final RippleAmount amount = iterator.next();
  242. if (amount.getCurrency().equals("XRP") && trade.getFee().equals(amount.getValue().negate())) {
  243. // XRP balance change is just the fee - it should not be part of the currency pair
  244. // considerations
  245. iterator.remove();
  246. }
  247. }
  248. if (balanceChanges.size() != 2) {
  249. logger.warn(
  250. "for hash[{}] of type[{}] balance changes section should contains 2 currency amounts but found {}",
  251. trade.getHash(),
  252. trade.getClass().getSimpleName(),
  253. balanceChanges);
  254. return null;
  255. }
  256. // There is no way of telling the original entered base or counter currency - Ripple just
  257. // provides 2 currency adjustments.
  258. // Check if TradeHistoryParams expressed a preference, otherwise arrange the currencies in the
  259. // order they are supplied.
  260. final Collection<Currency> preferredBase, preferredCounter;
  261. if (params instanceof RippleTradeHistoryPreferredCurrencies) {
  262. final RippleTradeHistoryPreferredCurrencies rippleParams =
  263. (RippleTradeHistoryPreferredCurrencies) params;
  264. preferredBase = rippleParams.getPreferredBaseCurrency();
  265. preferredCounter = rippleParams.getPreferredCounterCurrency();
  266. } else {
  267. preferredBase = preferredCounter = EMPTY_CURRENCY_SET;
  268. }
  269. final RippleAmount base, counter;
  270. if (preferredBase.contains(Currency.getInstance(balanceChanges.get(0).getCurrency()))) {
  271. base = balanceChanges.get(0);
  272. counter = balanceChanges.get(1);
  273. } else if (preferredBase.contains(Currency.getInstance(balanceChanges.get(1).getCurrency()))) {
  274. base = balanceChanges.get(1);
  275. counter = balanceChanges.get(0);
  276. } else if (preferredCounter.contains(
  277. Currency.getInstance(balanceChanges.get(0).getCurrency()))) {
  278. counter = balanceChanges.get(0);
  279. base = balanceChanges.get(1);
  280. } else if (preferredCounter.contains(
  281. Currency.getInstance(balanceChanges.get(1).getCurrency()))) {
  282. counter = balanceChanges.get(1);
  283. base = balanceChanges.get(0);
  284. } else if ((params instanceof TradeHistoryParamCurrencyPair)
  285. && (((TradeHistoryParamCurrencyPair) params).getCurrencyPair() != null)) {
  286. // Searching for a specific currency pair - use this direction
  287. final CurrencyPair pair = ((TradeHistoryParamCurrencyPair) params).getCurrencyPair();
  288. if (pair.base.getCurrencyCode().equals(balanceChanges.get(0).getCurrency())
  289. && pair.counter.getCurrencyCode().equals(balanceChanges.get(1).getCurrency())) {
  290. base = balanceChanges.get(0);
  291. counter = balanceChanges.get(1);
  292. } else if (pair.base.getCurrencyCode().equals(balanceChanges.get(1).getCurrency())
  293. && pair.counter.getCurrencyCode().equals(balanceChanges.get(0).getCurrency())) {
  294. base = balanceChanges.get(1);
  295. counter = balanceChanges.get(0);
  296. } else {
  297. // Unexpected: this should have been filtered out in
  298. // RippleTradeServiceRaw.getTradesForAccount(..) method.
  299. throw new IllegalStateException(
  300. String.format(
  301. "trade history param currency filter specified %s but trade query returned %s and %s",
  302. pair, balanceChanges.get(0).getCurrency(), balanceChanges.get(1).getCurrency()));
  303. }
  304. } else { // select the currency direction as return from the API
  305. base = balanceChanges.get(0);
  306. counter = balanceChanges.get(1);
  307. }
  308. final OrderType type;
  309. if (base.getValue().signum() == 1) {
  310. type = OrderType.BID;
  311. } else {
  312. type = OrderType.ASK;
  313. }
  314. final String currencyPairString = base.getCurrency() + "/" + counter.getCurrency();
  315. final CurrencyPair currencyPair =
  316. CurrencyPairDeserializer.getCurrencyPairFromString(currencyPairString);
  317. // Ripple has 2 types of fee.
  318. //
  319. // (a) Transaction fee is a network charge levied in XRP.
  320. // https://wiki.ripple.com/Transaction_Fee
  321. //
  322. // (b) Transfer fee charged by the issuer levied in the currency of traded instrument. Whoever
  323. // sends an asset that has a transfer fee pays the fee, the receiver does not incur a charge.
  324. // https://wiki.ripple.com/Transit_Fee
  325. // https://ripple.com/knowledge_center/transfer-fees/
  326. // Ripple supplies XRP with net quantity and price, must apply these to the
  327. // trade as gross amounts to ensure the same as the other XChange connections.
  328. final BigDecimal baseTransferFee =
  329. RippleTradeServiceRaw.getExpectedTransferFee(
  330. transferFeeSource, base.getCounterparty(), base.getCurrency(), base.getValue(), type);
  331. final BigDecimal baseValue = base.getValue().abs().subtract(baseTransferFee);
  332. final OrderType counterDirection;
  333. if (type == OrderType.BID) {
  334. counterDirection = OrderType.ASK;
  335. } else {
  336. counterDirection = OrderType.BID;
  337. }
  338. final BigDecimal counterTransferFee =
  339. RippleTradeServiceRaw.getExpectedTransferFee(
  340. transferFeeSource,
  341. counter.getCounterparty(),
  342. counter.getCurrency(),
  343. counter.getValue(),
  344. counterDirection);
  345. final BigDecimal counterValue = counter.getValue().abs().subtract(counterTransferFee);
  346. // Account for transaction fee in quantities.
  347. final BigDecimal transactionFee = trade.getFee();
  348. final BigDecimal quantity;
  349. if (base.getCurrency().equals("XRP")) {
  350. if (type == OrderType.BID) {
  351. quantity = baseValue.add(transactionFee);
  352. } else { // OrderType.ASK
  353. quantity = baseValue.subtract(transactionFee);
  354. }
  355. } else {
  356. quantity = baseValue;
  357. }
  358. final BigDecimal counterAmount;
  359. if (counter.getCurrency().equals("XRP")) {
  360. if (type == OrderType.ASK) {
  361. counterAmount = counterValue.add(transactionFee);
  362. } else { // OrderType.BID
  363. counterAmount = counterValue.subtract(transactionFee);
  364. }
  365. } else {
  366. counterAmount = counterValue;
  367. }
  368. // need to provide rounding scale to prevent ArithmeticException
  369. final BigDecimal price = counterAmount.divide(quantity, scale, RoundingMode.HALF_UP);
  370. final String orderId = Long.toString(trade.getOrderId());
  371. final RippleUserTrade.Builder builder =
  372. (RippleUserTrade.Builder)
  373. new RippleUserTrade.Builder()
  374. .currencyPair(currencyPair)
  375. .feeAmount(transactionFee)
  376. .feeCurrency(Currency.XRP)
  377. .id(trade.getHash())
  378. .orderId(orderId)
  379. .price(price.stripTrailingZeros())
  380. .timestamp(trade.getTimestamp())
  381. .originalAmount(quantity.stripTrailingZeros())
  382. .type(type);
  383. builder.baseTransferFee(baseTransferFee.abs());
  384. builder.counterTransferFee(counterTransferFee.abs());
  385. if (base.getCounterparty().length() > 0) {
  386. builder.baseCounterparty(base.getCounterparty());
  387. }
  388. if (counter.getCounterparty().length() > 0) {
  389. builder.counterCounterparty(counter.getCounterparty());
  390. }
  391. return builder.build();
  392. }
  393. public static UserTrades adaptTrades(
  394. final Collection<IRippleTradeTransaction> tradesForAccount,
  395. final TradeHistoryParams params,
  396. final RippleAccountService accountService,
  397. final int roundingScale)
  398. throws IOException {
  399. final List<UserTrade> trades = new ArrayList<>();
  400. for (final IRippleTradeTransaction orderDetails : tradesForAccount) {
  401. final UserTrade trade = adaptTrade(orderDetails, params, accountService, roundingScale);
  402. if (trade == null) {
  403. // any issue should have been reported by adaptTrade
  404. } else {
  405. trades.add(trade);
  406. }
  407. }
  408. return new UserTrades(trades, TradeSortType.SortByTimestamp);
  409. }
  410. }