
org.knowm.xchange.simulated.MatchingEngine Maven / Gradle / Ivy
package org.knowm.xchange.simulated;
import static java.math.BigDecimal.ZERO;
import static java.math.RoundingMode.HALF_UP;
import static java.util.UUID.randomUUID;
import static java.util.stream.Collectors.toList;
import static org.knowm.xchange.dto.Order.OrderType.ASK;
import static org.knowm.xchange.dto.Order.OrderType.BID;
import com.google.common.collect.*;
import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.function.Consumer;
import java.util.stream.Stream;
import org.knowm.xchange.currency.CurrencyPair;
import org.knowm.xchange.dto.Order;
import org.knowm.xchange.dto.Order.OrderType;
import org.knowm.xchange.dto.marketdata.OrderBook;
import org.knowm.xchange.dto.marketdata.Ticker;
import org.knowm.xchange.dto.marketdata.Trade;
import org.knowm.xchange.dto.trade.LimitOrder;
import org.knowm.xchange.dto.trade.MarketOrder;
import org.knowm.xchange.dto.trade.UserTrade;
import org.knowm.xchange.exceptions.ExchangeException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The "exchange" which backs {@link SimulatedExchange}.
*
* @author Graham Crockford
*/
final class MatchingEngine {
private static final Logger LOGGER = LoggerFactory.getLogger(MatchingEngine.class);
private static final BigDecimal FEE_RATE = new BigDecimal("0.001");
private static final int TRADE_HISTORY_SIZE = 50;
private final AccountFactory accountFactory;
private final CurrencyPair currencyPair;
private final int priceScale;
private final BigDecimal minimumAmount;
private final Consumer onFill;
private final List asks = new LinkedList<>();
private final List bids = new LinkedList<>();
private final Deque publicTrades = new ConcurrentLinkedDeque<>();
private final Multimap userTrades = LinkedListMultimap.create();
private volatile Ticker ticker = new Ticker.Builder().build();
MatchingEngine(
AccountFactory accountFactory,
CurrencyPair currencyPair,
int priceScale,
BigDecimal minimumAmount) {
this(accountFactory, currencyPair, priceScale, minimumAmount, f -> {});
}
MatchingEngine(
AccountFactory accountFactory,
CurrencyPair currencyPair,
int priceScale,
BigDecimal minimumAmount,
Consumer onFill) {
this.accountFactory = accountFactory;
this.currencyPair = currencyPair;
this.priceScale = priceScale;
this.minimumAmount = minimumAmount;
this.onFill = onFill;
}
public synchronized LimitOrder postOrder(String apiKey, Order original) {
LOGGER.debug("User {} posting order: {}", apiKey, original);
validate(original);
Account account = accountFactory.get(apiKey);
checkBalance(original, account);
BookOrder takerOrder = BookOrder.fromOrder(original, apiKey);
switch (takerOrder.getType()) {
case ASK:
LOGGER.debug("Matching against bids");
chewBook(bids, takerOrder);
if (!takerOrder.isDone()) {
if (original instanceof MarketOrder) {
throw new ExchangeException("Cannot fulfil order. No buyers.");
}
insertIntoBook(asks, takerOrder, ASK, account);
}
break;
case BID:
LOGGER.debug("Matching against asks");
chewBook(asks, takerOrder);
if (!takerOrder.isDone()) {
if (original instanceof MarketOrder) {
throw new ExchangeException("Cannot fulfil order. No sellers.");
}
insertIntoBook(bids, takerOrder, BID, account);
}
break;
default:
throw new ExchangeException("Unsupported order type: " + takerOrder.getType());
}
return takerOrder.toOrder(currencyPair);
}
private void validate(Order order) {
if (order.getOriginalAmount().compareTo(minimumAmount) < 0) {
throw new ExchangeException(
"Trade amount is " + order.getOriginalAmount() + ", minimum is " + minimumAmount);
}
if (order instanceof LimitOrder) {
LimitOrder limitOrder = (LimitOrder) order;
if (limitOrder.getLimitPrice() == null) {
throw new ExchangeException("No price");
}
if (limitOrder.getLimitPrice().compareTo(ZERO) <= 0) {
throw new ExchangeException(
"Limit price is " + limitOrder.getLimitPrice() + ", must be positive");
}
int scale = limitOrder.getLimitPrice().stripTrailingZeros().scale();
if (scale > priceScale) {
throw new ExchangeException("Price scale is " + scale + ", maximum is " + priceScale);
}
}
}
private void checkBalance(Order order, Account account) {
if (order instanceof LimitOrder) {
account.checkBalance((LimitOrder) order);
} else {
BigDecimal marketCostOrProceeds =
marketCostOrProceeds(order.getType(), order.getOriginalAmount());
BigDecimal marketAmount =
order.getType().equals(OrderType.BID) ? marketCostOrProceeds : order.getOriginalAmount();
account.checkBalance(order, marketAmount);
}
}
private void insertIntoBook(
List book, BookOrder order, OrderType type, Account account) {
int i = 0;
boolean insert = false;
Iterator iter = book.iterator();
while (iter.hasNext()) {
BookLevel level = iter.next();
int signum = level.getPrice().compareTo(order.getLimitPrice());
if (signum == 0) {
level.getOrders().add(order);
return;
} else if (signum < 0 && type == BID || signum > 0 && type == ASK) {
insert = true;
break;
}
i++;
}
account.reserve(order.toOrder(currencyPair));
BookLevel newLevel = new BookLevel(order.getLimitPrice());
newLevel.getOrders().add(order);
if (insert) {
book.add(i, newLevel);
} else {
book.add(newLevel);
}
ticker = newTickerFromBook().last(ticker.getLast()).build();
}
private Ticker.Builder newTickerFromBook() {
return new Ticker.Builder()
.ask(asks.isEmpty() ? null : asks.get(0).getPrice())
.bid(bids.isEmpty() ? null : bids.get(0).getPrice());
}
/**
* Calculates the total cost or proceeds at market price of the specified bid/ask amount.
*
* @param orderType Ask or bid.
* @param amount The amount.
* @return The market cost/proceeds
* @throws ExchangeException If there is insufficient liquidity.
*/
public BigDecimal marketCostOrProceeds(OrderType orderType, BigDecimal amount) {
BigDecimal remaining = amount;
BigDecimal cost = ZERO;
List orderbookSide = orderType.equals(BID) ? asks : bids;
for (BookOrder order :
FluentIterable.from(orderbookSide).transformAndConcat(BookLevel::getOrders)) {
BigDecimal available = order.getRemainingAmount();
BigDecimal tradeAmount = remaining.compareTo(available) >= 0 ? available : remaining;
BigDecimal tradeCost = tradeAmount.multiply(order.getLimitPrice());
cost = cost.add(tradeCost);
remaining = remaining.subtract(tradeAmount);
if (remaining.compareTo(ZERO) == 0) return cost;
}
throw new ExchangeException("Insufficient liquidity in book");
}
public synchronized Level3OrderBook book() {
return new Level3OrderBook(
FluentIterable.from(asks)
.transformAndConcat(BookLevel::getOrders)
.transform(o -> o.toOrder(currencyPair))
.toList(),
FluentIterable.from(bids)
.transformAndConcat(BookLevel::getOrders)
.transform(o -> o.toOrder(currencyPair))
.toList());
}
public Ticker ticker() {
return ticker;
}
public List publicTrades() {
return FluentIterable.from(publicTrades).transform(t -> Trade.Builder.from(t).build()).toList();
}
public synchronized List tradeHistory(String apiKey) {
return ImmutableList.copyOf(userTrades.get(apiKey));
}
private void chewBook(Iterable makerOrders, BookOrder takerOrder) {
Iterator levelIter = makerOrders.iterator();
while (levelIter.hasNext()) {
BookLevel level = levelIter.next();
Iterator orderIter = level.getOrders().iterator();
while (orderIter.hasNext() && !takerOrder.isDone()) {
BookOrder makerOrder = orderIter.next();
LOGGER.debug("Matching against maker order {}", makerOrder);
if (!makerOrder.matches(takerOrder)) {
LOGGER.debug("Ran out of maker orders at this price");
return;
}
BigDecimal tradeAmount =
takerOrder.getRemainingAmount().compareTo(makerOrder.getRemainingAmount()) > 0
? makerOrder.getRemainingAmount()
: takerOrder.getRemainingAmount();
LOGGER.debug("Matches for {}", tradeAmount);
matchOff(takerOrder, makerOrder, tradeAmount);
if (makerOrder.isDone()) {
LOGGER.debug("Maker order removed from book");
orderIter.remove();
if (level.getOrders().isEmpty()) {
levelIter.remove();
}
}
}
}
}
private void matchOff(BookOrder takerOrder, BookOrder makerOrder, BigDecimal tradeAmount) {
Date timestamp = new Date();
UserTrade takerTrade =
new UserTrade.Builder()
.currencyPair(currencyPair)
.id(randomUUID().toString())
.originalAmount(tradeAmount)
.price(makerOrder.getLimitPrice())
.timestamp(timestamp)
.type(takerOrder.getType())
.orderId(takerOrder.getId())
.feeAmount(
takerOrder.getType() == ASK
? tradeAmount.multiply(makerOrder.getLimitPrice()).multiply(FEE_RATE)
: tradeAmount.multiply(FEE_RATE))
.feeCurrency(takerOrder.getType() == ASK ? currencyPair.counter : currencyPair.base)
.build();
LOGGER.debug("Created taker trade: {}", takerTrade);
accumulate(takerOrder, takerTrade);
OrderType makerType = takerOrder.getType() == OrderType.ASK ? OrderType.BID : OrderType.ASK;
UserTrade makerTrade =
new UserTrade.Builder()
.currencyPair(currencyPair)
.id(randomUUID().toString())
.originalAmount(tradeAmount)
.price(makerOrder.getLimitPrice())
.timestamp(timestamp)
.type(makerType)
.orderId(makerOrder.getId())
.feeAmount(
makerType == ASK
? tradeAmount.multiply(makerOrder.getLimitPrice()).multiply(FEE_RATE)
: tradeAmount.multiply(FEE_RATE))
.feeCurrency(makerType == ASK ? currencyPair.counter : currencyPair.base)
.build();
LOGGER.debug("Created maker trade: {}", makerOrder);
accumulate(makerOrder, makerTrade);
recordFill(new Fill(takerOrder.getApiKey(), takerTrade, true));
recordFill(new Fill(makerOrder.getApiKey(), makerTrade, false));
ticker = newTickerFromBook().last(makerOrder.getLimitPrice()).build();
}
private void accumulate(BookOrder bookOrder, UserTrade trade) {
BigDecimal amount = trade.getOriginalAmount();
BigDecimal price = trade.getPrice();
BigDecimal newTotal = bookOrder.getCumulativeAmount().add(amount);
if (bookOrder.getCumulativeAmount().compareTo(ZERO) == 0) {
bookOrder.setAveragePrice(price);
} else {
bookOrder.setAveragePrice(
bookOrder
.getAveragePrice()
.multiply(bookOrder.getCumulativeAmount())
.add(price.multiply(amount))
.divide(newTotal, priceScale, HALF_UP));
}
bookOrder.setCumulativeAmount(newTotal);
bookOrder.setFee(bookOrder.getFee().add(trade.getFeeAmount()));
}
public synchronized List openOrders(String apiKey) {
return Stream.concat(asks.stream(), bids.stream())
.flatMap(v -> v.getOrders().stream())
.filter(o -> o.getApiKey().equals(apiKey))
.sorted(Ordering.natural().onResultOf(BookOrder::getTimestamp).reversed())
.map(o -> o.toOrder(currencyPair))
.collect(toList());
}
public synchronized OrderBook level2() {
return new OrderBook(new Date(), accumulateBookSide(asks), accumulateBookSide(bids));
}
private List accumulateBookSide(List book) {
BigDecimal price = null;
BigDecimal amount = ZERO;
List result = new ArrayList<>();
Iterator iter = book.stream().flatMap(v -> v.getOrders().stream()).iterator();
while (iter.hasNext()) {
BookOrder bookOrder = iter.next();
amount = amount.add(bookOrder.getRemainingAmount());
if (price != null && bookOrder.getLimitPrice().compareTo(price) != 0) {
result.add(
new LimitOrder.Builder(ASK, currencyPair)
.originalAmount(amount)
.limitPrice(price)
.build());
amount = ZERO;
}
price = bookOrder.getLimitPrice();
}
if (price != null) {
result.add(
new LimitOrder.Builder(ASK, currencyPair)
.originalAmount(amount)
.limitPrice(price)
.build());
}
return result;
}
private void recordFill(Fill fill) {
// XChange is unusual in this respect (see https://github.com/knowm/XChange/issues/2468)
if (!fill.isTaker()) {
publicTrades.push(fill.getTrade());
if (publicTrades.size() > TRADE_HISTORY_SIZE) {
publicTrades.removeLast();
}
}
userTrades.put(fill.getApiKey(), fill.getTrade());
accountFactory.get(fill.getApiKey()).fill(fill.getTrade(), !fill.isTaker());
onFill.accept(fill);
}
public void cancelOrder(String orderId, Order.OrderType type) {
switch (type) {
case ASK:
asks.stream()
.forEach(
bookLevel ->
bookLevel.getOrders().removeIf(bookOrder -> bookOrder.getId().equals(orderId)));
break;
case BID:
bids.stream()
.forEach(
bookLevel ->
bookLevel.getOrders().removeIf(bookOrder -> bookOrder.getId().equals(orderId)));
break;
default:
throw new ExchangeException("Unsupported order type: " + type);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy