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

info.bitrich.xchangestream.kraken.KrakenStreamingAdapters Maven / Gradle / Ivy

There is a newer version: 5.2.1
Show newest version
package info.bitrich.xchangestream.kraken;

import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.node.*;
import com.google.common.collect.Streams;
import com.google.common.collect.*;
import org.knowm.xchange.dto.*;
import org.knowm.xchange.dto.marketdata.*;
import org.knowm.xchange.dto.trade.*;
import org.knowm.xchange.instrument.*;
import org.knowm.xchange.kraken.*;
import org.knowm.xchange.kraken.dto.trade.*;
import org.knowm.xchange.utils.*;
import org.slf4j.*;

import java.math.*;
import java.util.*;
import java.util.concurrent.atomic.*;
import java.util.stream.*;

import static info.bitrich.xchangestream.kraken.KrakenStreamingChecksum.*;

/** Kraken streaming adapters */
public class KrakenStreamingAdapters {
  private static final Logger LOG = LoggerFactory.getLogger(KrakenStreamingAdapters.class);

  static final String ASK_SNAPSHOT = "as";
  static final String ASK_UPDATE = "a";

  static final String BID_SNAPSHOT = "bs";
  static final String BID_UPDATE = "b";

  static final String CHECKSUM = "c";

  private static void updateInBook(
      int depth,
      Instrument instrument,
      Order.OrderType orderType,
      JsonNode currentNode,
      String key,
      TreeSet target) {
    adaptLimitOrders(instrument, orderType, currentNode.get(key))
        .forEachRemaining(
            limitOrder -> {
              target.removeIf(it -> it.getLimitPrice().compareTo(limitOrder.getLimitPrice()) == 0);
              if (limitOrder.getOriginalAmount().compareTo(BigDecimal.ZERO) != 0) {
                target.add(limitOrder);
              }
            });
    while (target.size() > depth) {
      LimitOrder last = target.last();
      target.remove(last);
    }
  }

  public static OrderBook adaptOrderbookMessage(
      int depth,
      TreeSet bids,
      TreeSet asks,
      Instrument instrument,
      ArrayNode arrayNode) {
    final AtomicLong expectedChecksum = new AtomicLong(0);
    final boolean awaitingSnapshot = (bids.isEmpty() && asks.isEmpty());
    arrayNode
        .elements()
        .forEachRemaining(
            currentNode -> {
              if (awaitingSnapshot) {
                if (currentNode.has(BID_SNAPSHOT) && currentNode.has(ASK_SNAPSHOT)) {
                  LOG.info("Received {} snapshot, clearing book", instrument);
                  updateInBook(
                      depth, instrument, Order.OrderType.BID, currentNode, BID_SNAPSHOT, bids);
                  updateInBook(
                      depth, instrument, Order.OrderType.ASK, currentNode, ASK_SNAPSHOT, asks);
                }
              } else {
                if (currentNode.has(BID_UPDATE)) {
                  updateInBook(
                      depth, instrument, Order.OrderType.BID, currentNode, BID_UPDATE, bids);
                }
                if (currentNode.has(ASK_UPDATE)) {
                  updateInBook(
                      depth, instrument, Order.OrderType.ASK, currentNode, ASK_UPDATE, asks);
                }
              }
              if (!awaitingSnapshot && currentNode.has(CHECKSUM)) {
                expectedChecksum.set(currentNode.get(CHECKSUM).asLong());
              }
            });
    if (bids.isEmpty() && asks.isEmpty()) {
      LOG.info("Ignoring {} message {}, awaiting snapshot", instrument, arrayNode);
    }
    long localChecksum = createCrcChecksum(asks, bids);
    if (expectedChecksum.get() > 0 && expectedChecksum.get() != localChecksum) {
      LOG.warn(
          "{} checksum does not match, expected {} but local checksum is {}",
          instrument,
          expectedChecksum.get(),
          localChecksum);
      throw new IllegalStateException("Checksum did not match");
    } else if (expectedChecksum.get() == 0) {
      LOG.debug("Skipping {} checksum validation, no expected checksum in message", instrument);
    } else if (bids.size() > 0
        && asks.size() > 0
        && bids.first().getLimitPrice().compareTo(asks.first().getLimitPrice()) >= 0) {
      throw new IllegalStateException(
          "CROSSED book "
              + instrument
              + " "
              + bids.first().getLimitPrice()
              + " >= "
              + asks.first().getLimitPrice());
    }
    final Date lastTime =
        Stream.concat(asks.stream(), bids.stream())
            .map(LimitOrder::getTimestamp)
            .max(Date::compareTo)
            .orElse(null);
    return new OrderBook(lastTime, Lists.newArrayList(asks), Lists.newArrayList(bids), true);
  }

  /**
   * Adapt a JsonNode to a Stream of limit orders, the node past in here should be the body of a
   * a/b/as/bs key.
   */
  public static Iterator adaptLimitOrders(
      Instrument instrument, Order.OrderType orderType, JsonNode node) {
    if (node == null || !node.isArray()) {
      return Collections.emptyIterator();
    }
    return Iterators.transform(
        node.elements(), jsonNode -> adaptLimitOrder(instrument, orderType, jsonNode));
  }

  /** Adapt a JsonNode containing two decimals into a LimitOrder */
  public static LimitOrder adaptLimitOrder(
      Instrument instrument, Order.OrderType orderType, JsonNode node) {
    if (node == null || !node.isArray()) {
      return null;
    }
    Iterator iterator = node.elements();
    BigDecimal price = nextNodeAsDecimal(iterator);
    BigDecimal volume = nextNodeAsDecimal(iterator);
    Date timestamp = nextNodeAsDate(iterator);
    return new LimitOrder(orderType, volume, instrument, null, timestamp, price);
  }

  /** Adapt an ArrayNode containing a ticker message into a Ticker */
  public static Ticker adaptTickerMessage(Instrument instrument, ArrayNode arrayNode) {
    return Streams.stream(arrayNode.elements())
        .filter(JsonNode::isObject)
        .map(
            tickerNode -> {
              ArrayNode askArray = (ArrayNode) tickerNode.get("a");
              ArrayNode bidArray = (ArrayNode) tickerNode.get("b");
              Iterator closeIterator = tickerNode.get("c").iterator();
              Iterator volumeIterator = tickerNode.get("v").iterator();
              Iterator vwapIterator = tickerNode.get("p").iterator();
              Iterator lowPriceIterator = tickerNode.get("l").iterator();
              Iterator highPriceIterator = tickerNode.get("h").iterator();
              Iterator openPriceIterator = tickerNode.get("o").iterator();

              // Move iterators forward here required, this ignores the first field if the desired
              // value is in the second element.
              vwapIterator.next();
              volumeIterator.next();

              return new Ticker.Builder()
                  .open(nextNodeAsDecimal(openPriceIterator))
                  .ask(arrayNodeItemAsDecimal(askArray, 0))
                  .bid(arrayNodeItemAsDecimal(bidArray, 0))
                  .askSize(arrayNodeItemAsDecimal(askArray, 2))
                  .bidSize(arrayNodeItemAsDecimal(bidArray, 2))
                  .last(nextNodeAsDecimal(closeIterator))
                  .high(nextNodeAsDecimal(highPriceIterator))
                  .low(nextNodeAsDecimal(lowPriceIterator))
                  .vwap(nextNodeAsDecimal(vwapIterator))
                  .volume(nextNodeAsDecimal(volumeIterator))
                  .instrument(instrument)
                  .build();
            })
        .findFirst()
        .orElse(null);
  }

  /** Adapt an ArrayNode containing a spread message into a Ticker */
  public static Ticker adaptSpreadMessage(Instrument instrument, ArrayNode arrayNode) {
    ArrayNode data = (ArrayNode) arrayNode.get(1);

    return new Ticker.Builder()
            .ask(arrayNodeItemAsDecimal(data, 1))
            .bid(arrayNodeItemAsDecimal(data, 0))
            .askSize(arrayNodeItemAsDecimal(data, 4))
            .bidSize(arrayNodeItemAsDecimal(data, 3))
            .timestamp(DateUtils.fromMillisUtc((long) (Double.parseDouble(data.get(2).textValue()) * 1000)))
            .instrument(instrument)
            .build();
  }

  /** Adapt an JsonNode into a list of Trade */
  public static List adaptTrades(Instrument instrument, JsonNode arrayNode) {
    return Streams.stream(arrayNode.elements())
        .filter(JsonNode::isArray)
        .flatMap(
            innerNode ->
                Streams.stream(innerNode.elements())
                    .map(inner -> KrakenStreamingAdapters.adaptTrade(instrument, inner)))
        .collect(Collectors.toList());
  }

  /** Adapt an JsonNode into a single Trade */
  public static Trade adaptTrade(Instrument instrument, JsonNode arrayNode) {
    if (arrayNode == null || !arrayNode.isArray()) {
      return null;
    }
    Iterator iterator = arrayNode.iterator();
    return new Trade.Builder()
        .price(nextNodeAsDecimal(iterator))
        .originalAmount(nextNodeAsDecimal(iterator))
        .timestamp(nextNodeAsDate(iterator))
        .type(nextNodeAsOrderType(iterator))
        .instrument(instrument)
        .build();
  }

  /**
   * Returns the element at index in arrayNode as a BigDecimal. Retuns null if the arrayNode is null
   * or index does not exist.
   */
  private static BigDecimal arrayNodeItemAsDecimal(ArrayNode arrayNode, int index) {
    if (arrayNode == null) {
      return null;
    }
    JsonNode itemNode = arrayNode.get(index);
    if (itemNode == null) {
      return null;
    }
    return new BigDecimal(itemNode.asText());
  }

  /**
   * Checks if a iterator has next node and returns the value as a BigDecimal. Returns null if the
   * iterator has no next value or the given iterator is null.
   */
  private static BigDecimal nextNodeAsDecimal(Iterator iterator) {
    if (iterator == null || !iterator.hasNext()) {
      return null;
    }
    return new BigDecimal(iterator.next().textValue());
  }

  /**
   * Checks if a iterator has next node and returns the value as a Date using the long value as
   * timestamp. Returns null if the iterator has no next value or the given iterator is null.
   */
  private static Date nextNodeAsDate(Iterator iterator) {
    if (iterator == null || !iterator.hasNext()) {
      return null;
    }
    return DateUtils.fromMillisUtc(
            (long) (Double.parseDouble(iterator.next().textValue()) * 1000));
  }

  /**
   * Checks if a iterator has next node and returns the value as a Date using the long value as
   * timestamp. Returns null if the iterator has no next value or the given iterator is null.
   */
  private static Order.OrderType nextNodeAsOrderType(Iterator iterator) {
    if (iterator == null || !iterator.hasNext()) {
      return null;
    }
    return KrakenAdapters.adaptOrderType(KrakenType.fromString(iterator.next().textValue()));
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy