/** * BidEngine.java * * Copyright 2015 the original author or authors. * * We licenses this file to you 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.apache.niolex.common.bid.engine; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import java.util.TreeMap; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import org.apache.niolex.common.bid.bean.Bid; import org.apache.niolex.common.bid.bean.BidQueue; import org.apache.niolex.common.bid.bean.Stock; import org.apache.niolex.common.bid.bean.Trade; import org.apache.niolex.commons.collection.CollectionUtil; /** * The bid engine class process all the bids for one stock. * * @author <a href="mailto:xiejiyun@foxmail.com">Xie, Jiyun</a> * @version 1.0.0 * @since 2015-7-9 */ public class BidEngine implements Runnable { private static final int BUY = 0; private static final int SELL = 1; // Map BidPrice => BidQueue @SuppressWarnings("unchecked") private final TreeMap<Integer, BidQueue>[] queueMaps = new TreeMap[] { new TreeMap<Integer, BidQueue>(), new TreeMap<Integer, BidQueue>() }; private final BlockingQueue<Bid> inputQueue = new LinkedBlockingQueue<Bid>(); // Map BidID => Bid private final Map<Long, Bid> tradeMap = new HashMap<Long, Bid>(); private final Stock stock; private final StockBoard board; private volatile boolean isWorking = true; private Thread runner = null; /** * Construct a new bid engine. * * @param stock the stock this bid engine is processing * @param board the stock board */ public BidEngine(Stock stock, StockBoard board) { super(); this.stock = stock; this.board = board; } /** * Put a new Bid into this Bid engine. * * @param b * the new Bid */ public void putBid(Bid b) { inputQueue.add(b); } /** * Query the Bid Id from this Bid engine. * * @param bidId the bid Id * @return the Bid if it's being processing, null if it's done or not exist or in the input queue. */ public Bid queryBid(long bidId) { return tradeMap.get(bidId); } /** * Start this bid engine. */ public synchronized void startEngine() { if (runner != null) return; isWorking = true; runner = new Thread(this); runner.start(); } /** * Stop this bid engine. */ public synchronized void stopEngine() { if (runner == null) return; isWorking = false; runner.interrupt(); runner = null; } /** * The working method, processing the input queue, and generate trade. * This is the override of super method. * * @see java.lang.Runnable#run() */ @Override public void run() { Bid b = null; while (isWorking) { // Take out a new bid. try { b = inputQueue.take(); } catch (InterruptedException e) { continue; } // Process the new bid. processBid(b); while (processTrade()) { ; } } } /** * Process this Bid. * * @param b * the bid */ private void processBid(Bid b) { Long bidId = b.getBidId(); Integer price = b.getPrice(); TreeMap<Integer, BidQueue> queueMap = null; switch (b.getType()) { case BUY: queueMap = queueMaps[BUY]; break; case SELL: queueMap = queueMaps[SELL]; break; case CANCEL: Bid c = tradeMap.get(bidId); cancelBid(c, b.getAmount()); break; } if (queueMap != null) { BidQueue queue = CollectionUtil.getOrInit(queueMap, price, BidQueue.class); queue.offer(b); tradeMap.put(bidId, b); } } /** * Cancel some amount from the target bid. * * @param target * the bid to be canceled * @param cancelAmount * the amount to be canceled from the target */ private void cancelBid(Bid target, int cancelAmount) { int remain = target.getRemainAmount(); target.addCanceledAmount(remain > cancelAmount ? cancelAmount : remain); } /** * Process trade across buyers and sellers. * * @return true if maybe there is some more trade */ private boolean processTrade() { // Take the highest buyer. Entry<Integer, BidQueue> buyEntry = queueMaps[BUY].lastEntry(); // Take the lowest seller. Entry<Integer, BidQueue> sellEntry = queueMaps[SELL].firstEntry(); if (buyEntry == null || sellEntry == null) { return false; } if (buyEntry.getKey().intValue() < sellEntry.getKey().intValue()) { return false; } BidQueue buyQueue = buyEntry.getValue(); BidQueue sellQueue = sellEntry.getValue(); while (!buyQueue.isEmpty()) { Bid buy = buyQueue.peek(); if (buy.getRemainAmount() <= 0) { buyQueue.poll(); bidDone(buy); continue; } if (sellQueue.isEmpty()) { break; } while (!sellQueue.isEmpty()) { Bid sell = sellQueue.peek(); if (sell.getRemainAmount() <= 0) { sellQueue.poll(); bidDone(sell); continue; } // Start to generate trade. long buyBidId = buy.getBidId(); long sellBidId = sell.getBidId(); int buyAmount = buy.getRemainAmount(); int sellAmount = sell.getRemainAmount(); Trade.Type tradeType; int tradePrice; int tradeAmount; if (buyBidId < sellBidId) { tradeType = Trade.Type.IN; tradePrice = buy.getPrice(); } else { tradeType = Trade.Type.OUT; tradePrice = sell.getPrice(); } boolean buyDone = false; boolean sellDone = false; if (buyAmount > sellAmount) { tradeAmount = sellAmount; // Sell bid will be done, remove it. sellDone = true; } else if (sellAmount > buyAmount) { tradeAmount = buyAmount; // Buy bid will be done, remove it. buyDone = true; } else { tradeAmount = sellAmount; // Both the buy bid and sell bid will be done. buyDone = sellDone = true; } Trade t = new Trade(IdGenerator.nextTradeId(), tradeType, sellBidId, buyBidId, tradePrice, tradeAmount); buy.addTrade(t); sell.addTrade(t); stock.addTradeAmount(tradeAmount); stock.setCurrentPrice(tradePrice); if (sellDone) { sellQueue.poll(); bidDone(sell); } if (buyDone) { buyQueue.poll(); bidDone(buy); break; } } } return true; } /** * The target bid is done, doing bookkeeping and notify the bid owner. * * @param target * the target bid */ private void bidDone(Bid target) { TreeMap<Integer, BidQueue> queueMap = queueMaps[target.getType() == Bid.Type.BUY ? BUY : SELL]; Integer price = target.getPrice(); BidQueue queue = queueMap.get(price); if (queue.isEmpty()) { // clean the queue map. queueMap.remove(price); } Long bidId = target.getBidId(); // Clean the trade map. tradeMap.remove(bidId); // Notify the board. board.bidDone(target); } /** * Get the current Bid Engine internal status. * * @return the status as string */ @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("Is working? ").append(isWorking).append(" "); sb.append("B[").append(queueMaps[BUY].size()).append("] "); sb.append("S[").append(queueMaps[SELL].size()).append("] "); sb.append("I[").append(inputQueue.size()).append("] "); sb.append("P[").append(tradeMap.size()).append("]"); return sb.toString(); } }