/**
* Copyright 2011 Archfirst
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.archfirst.bfexch.domain.trading;
import java.util.List;
import javax.inject.Inject;
import org.archfirst.bfexch.domain.marketdata.MarketDataEventPublisher;
import org.archfirst.bfexch.domain.marketdata.MarketDataRepository;
import org.archfirst.bfexch.domain.marketdata.MarketPrice;
import org.archfirst.bfexch.domain.marketdata.MarketPriceChanged;
import org.archfirst.bfexch.domain.trading.order.Execution;
import org.archfirst.bfexch.domain.trading.order.Order;
import org.archfirst.bfexch.domain.trading.order.OrderAccepted;
import org.archfirst.bfexch.domain.trading.order.OrderEventPublisher;
import org.archfirst.bfexch.domain.trading.order.OrderExecuted;
import org.archfirst.bfexch.domain.trading.order.OrderRepository;
import org.archfirst.bfexch.domain.trading.order.OrderStatus;
import org.archfirst.bfexch.domain.trading.order.OrderType;
import org.archfirst.bfexch.domain.util.Constants;
import org.archfirst.common.money.Money;
import org.archfirst.common.quantity.DecimalQuantity;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* MatchingEngine
*
* @author Naresh Bhatia
*/
public class MatchingEngine {
private static final Logger logger =
LoggerFactory.getLogger(MatchingEngine.class);
// ----- Commands -----
public void placeOrder(Order order) {
this.acceptOrder(order);
this.performMatching(order.getSymbol());
}
private void performMatching(String symbol) {
logger.debug("Pricing engine triggered for symbol {}", symbol);
MarketPrice marketPrice =
marketDataRepository.findMarketPrice(symbol);
Money preMatchingPrice = marketPrice.getPrice();
OrderBook orderBook = getOrderBook(symbol);
// Iterate through buy orders
for (Order buyOrder : orderBook.getBuyStack()) {
logger.debug("Trying to match buy order:\n{}", buyOrder);
MatchResult matchResult =
new MatchResult(false, NoMatchReason.priceMismatch);
// Iterate through sell orders to match the current buy order
for (Order sellOrder : orderBook.getSellStack()) {
// Skip sell order if it has been filled in a previous iteration
if (!sellOrder.isActive()) {
continue;
}
matchResult = matchOrder(buyOrder, sellOrder, marketPrice);
// Analyze match result and break out of inner loop if appropriate
if (matchResult.isMatch()) {
if (buyOrder.getStatus() == OrderStatus.Filled) {
logger.debug("Buy order filled, stop matching with sell orders");
break;
}
}
else { // no match
if (matchResult.noMatchReason == NoMatchReason.priceMismatch) {
logger.debug("Buy order did not match due to price mismatch, stop matching with sell orders");
break;
}
}
}
// If buy order did not match due to price mismatch, then break.
// (No other buy orders will match.)
if (matchResult.isMatch()==false &&
matchResult.getNoMatchReason()==NoMatchReason.priceMismatch) {
logger.debug("Buy order did not match due to price mismatch, stop matching other buy orders");
break;
}
}
// If market price has changed are a result of this run, publish the new price
if (!marketPrice.getPrice().eq(preMatchingPrice)) {
marketDataEventPublisher.publish(new MarketPriceChanged(marketPrice));
}
}
private MatchResult matchOrder(
Order buyOrder, Order sellOrder, MarketPrice marketPrice) {
logger.debug("Before match:\n{}\n{}", buyOrder, sellOrder);
if (isAllOrNoneRestricted(buyOrder, sellOrder)) {
logger.debug("No match: AllOrNone restriction");
return new MatchResult(false, NoMatchReason.allOrNone);
}
MatchResult matchResult =
new MatchResult(false, NoMatchReason.priceMismatch);
if (buyOrder.getType() == OrderType.Market) {
if (sellOrder.getType() == OrderType.Market) {
executeOrders(buyOrder, sellOrder, marketPrice.getPrice());
}
else { // sell order is limit order
executeOrders(buyOrder, sellOrder, sellOrder.getLimitPrice());
marketPrice.change(sellOrder.getLimitPrice());
}
matchResult = new MatchResult(true, null);
}
else { // buy order is a limit order
if (sellOrder.getType() == OrderType.Market) {
executeOrders(buyOrder, sellOrder, buyOrder.getLimitPrice());
marketPrice.change(buyOrder.getLimitPrice());
matchResult = new MatchResult(true, null);
}
else { // sell order is limit order
Money buyPrice = buyOrder.getLimitPrice();
Money sellPrice = sellOrder.getLimitPrice();
if (buyPrice.compareTo(sellPrice) >= 0) {
Money executionPrice =
buyPrice.plus(sellPrice).div(2, Constants.PRICE_SCALE);
executeOrders(buyOrder, sellOrder, executionPrice);
marketPrice.change(executionPrice);
matchResult = new MatchResult(true, null);
}
}
}
logger.debug("After match:\n{}\n{}", buyOrder, sellOrder);
return matchResult;
}
/**
* Is the specified match restricted by an AllOrNone condition on one of
* the orders.
* @param buyOrder
* @param sellOrder
* @return true if match cannot be made due to AllOrNone restriction
*/
private boolean isAllOrNoneRestricted(Order buyOrder, Order sellOrder) {
boolean restricted = false;
// Check for buy side restriction
if (buyOrder.isAllOrNone()) {
if (buyOrder.getLeavesQty().compareTo(sellOrder.getLeavesQty()) > 0) {
restricted = true;
}
}
// Check for sell side restriction
if (restricted == false && sellOrder.isAllOrNone()) {
if (sellOrder.getLeavesQty().compareTo(buyOrder.getLeavesQty()) > 0) {
restricted = true;
}
}
return restricted;
}
/**
* Sets order status to New and persists it.
*/
private void acceptOrder(Order order) {
order.accept(orderRepository);
orderEventPublisher.publish(new OrderAccepted(order));
}
private void executeOrders(Order buyOrder, Order sellOrder, Money price) {
DecimalQuantity quantity =
buyOrder.getLeavesQty().min(sellOrder.getLeavesQty());
DateTime executionTime = new DateTime();
this.executeOrder(buyOrder, executionTime, quantity, price);
this.executeOrder(sellOrder, executionTime, quantity, price);
}
/**
* Executes the order and adds an execution to it.
*/
private void executeOrder(
Order order,
DateTime executionTime,
DecimalQuantity executionQty,
Money price) {
Execution execution =
order.execute(orderRepository, executionTime, executionQty, price);
orderEventPublisher.publish(new OrderExecuted(execution));
}
// ----- Queries -----
public OrderBook getOrderBook(String symbol) {
OrderBook orderBook = new OrderBook();
List<Order> orders = orderRepository.findActiveOrdersForInstrument(symbol);
for (Order order : orders) {
orderBook.add(order);
}
return orderBook;
}
// ----- Attributes -----
@Inject private OrderRepository orderRepository;
@Inject private OrderEventPublisher orderEventPublisher;
@Inject private MarketDataRepository marketDataRepository;
@Inject private MarketDataEventPublisher marketDataEventPublisher;
// ----- Nested Types -----
private enum NoMatchReason {
allOrNone,
priceMismatch
}
private class MatchResult {
private final boolean match;
private final NoMatchReason noMatchReason;
public MatchResult(boolean match, NoMatchReason noMatchReason) {
this.match = match;
this.noMatchReason = noMatchReason;
}
public boolean isMatch() {
return match;
}
public NoMatchReason getNoMatchReason() {
return noMatchReason;
}
}
}