edu.caltechUcla.sselCassel.projects.jMarkets.server.control.ContinuousMarketEngine.java Source code

Java tutorial

Introduction

Here is the source code for edu.caltechUcla.sselCassel.projects.jMarkets.server.control.ContinuousMarketEngine.java

Source

/*
 * Copyright (C) 2005-2006, <a href="http://www.ssel.caltech.edu">SSEL</a>
 * <a href="http://www.cassel.ucla.edu">CASSEL</a>, Caltech/UCLA
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307,
 * USA.
 *
 * Project Authors: Raj Advani, Walter M. Yuan, and Peter Bossaerts
 * Email: jmarkets@ssel.caltech.edu
 */

/*
 * ContinuousMarketEngine.java
 *
 * Created on March 21, 2004, 4:37 PM
 */

package edu.caltechUcla.sselCassel.projects.jMarkets.server.control;

import java.util.*;
import java.sql.*;
import java.text.NumberFormat;
import edu.caltechUcla.sselCassel.projects.jMarkets.shared.interfaces.*;
import edu.caltechUcla.sselCassel.projects.jMarkets.shared.data.*;
import edu.caltechUcla.sselCassel.projects.jMarkets.shared.JMConstants;
import edu.caltechUcla.sselCassel.projects.jMarkets.server.data.*;
import edu.caltechUcla.sselCassel.projects.jMarkets.server.updates.*;
import edu.caltechUcla.sselCassel.projects.jMarkets.shared.data.def.SubjectDef;
import edu.caltechUcla.sselCassel.projects.jMarkets.shared.data.def.GroupDef;
import edu.caltechUcla.sselCassel.projects.jMarkets.shared.data.def.PeriodDef;
import edu.caltechUcla.sselCassel.projects.jMarkets.shared.data.def.MarketDef;
import edu.caltechUcla.sselCassel.projects.jMarkets.shared.data.offers.AbstractOffer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 *
 * @author  Raj Advani
 */
public class ContinuousMarketEngine implements TradeEngine {

    /**
     * Creates a new instance of ContinuousMarketEngine 
     */
    public ContinuousMarketEngine(DBWriter db) {
        log.info("The Continuous Market Engine has been instantiated");

        dbw = db;
        formatter = NumberFormat.getCurrencyInstance();
        rand = new Random();
        marketStates = new Hashtable();

        log.info("Continuous Market Engine has finished initialization procedures");
    }

    /**
     * Initialize the ContinuousMarketEngine to handle the given period. The ContinuousMarketEngine creates a MarketState
     *  object to hold the cached market data of the period. Return the array of Trader objects created
     *  for this period 
     */
    public Trader[] initPeriod(int sessionId, int periodNum, PeriodDef periodInfo, float[] initialCash,
            int[][] initialHoldings) {
        try {
            log.info("TradeServ is initializing a new MarketState object for period " + periodNum + " of session "
                    + sessionId);
            MarketDef marketInfo = periodInfo.getMarketInfo();
            SubjectDef subjectInfo = periodInfo.getSubjectInfo();
            GroupDef groupInfo = periodInfo.getGroupInfo();

            MarketState marketState = new MarketState(sessionId, periodNum, marketInfo, subjectInfo, groupInfo,
                    initialCash, initialHoldings);
            marketStates.put(new Integer(sessionId), marketState);

            return marketState.getTraders();
        } catch (Exception e) {
            log.error("TradeServ has failed to initialize", e);
        }
        return null;
    }

    public Trader[] getTraders(int sessionId) {
        MarketState marketState = (MarketState) marketStates.get(new Integer(sessionId));
        if (marketState == null) {
            log.warn("Cannot retrieve traders for session " + sessionId + " -- that session is not active!");
            return null;
        }

        return marketState.getTraders();
    }

    public MetricsUpdate[] getMetrics(int sessionId) {
        MarketState marketState = (MarketState) marketStates.get(new Integer(sessionId));
        if (marketState == null) {
            log.warn("Cannot retrieve traders for session " + sessionId + " -- that session is not active!");
            return null;
        }

        long[] transTime = marketState.getTransTime();
        int[] numTrans = marketState.getNumTrans();

        MetricsUpdate[] updates = new MetricsUpdate[transTime.length];
        for (int i = 0; i < updates.length; i++) {
            updates[i] = new MetricsUpdate(sessionId, i, numTrans[i], transTime[i]);
        }
        return updates;
    }

    public NumOffersUpdate[] getNumOffers(int sessionId) {
        MarketState marketState = (MarketState) marketStates.get(new Integer(sessionId));
        if (marketState == null) {
            log.warn("Cannot retrieve traders for session " + sessionId + " -- that session is not active!");
            return null;
        }

        int[] numOffers = marketState.getNumOffers();

        NumOffersUpdate[] updates = new NumOffersUpdate[numOffers.length];
        for (int i = 0; i < updates.length; i++) {
            updates[i] = new NumOffersUpdate(sessionId, i, numOffers[i]);
        }
        return updates;
    }

    /** Set the current time as the period start time for the given session and period */
    public boolean stampPeriodStartTime(int sessionId) {
        MarketState marketState = (MarketState) marketStates.get(new Integer(sessionId));
        if (marketState == null) {
            log.warn("Cannot set period start time for session " + sessionId + " -- that session is not active!");
            return false;
        }

        marketState.setPeriodStartTime(new java.util.Date().getTime());
        return true;
    }

    /** Terminate the given session */
    public boolean terminateSession(int sessionId) {
        marketStates.remove(new Integer(sessionId));
        return true;
    }

    /** Given a transaction request perform the following:
     *
     *  1) Ensure that the bid/ask is valid
     *  2) Check if the bid/ask results in a transaction
     *  3) If it does, add the transaction to the database
     *  4) Update the offer book
     *
     *  Creates an UpdateBasket object that tracks all the changes made in the course
     *  of this transaction so that it can be successfully rolled back in case something
     *  goes wrong. If the rollback is due to a SQL deadlock, then retry the transaction.
     *
     *  There are also two types of controls here for data integrity:
     *
     *  1) Key Validation ensures that the offer received was verified by a client who
     *     had up-to-date cash and security holding information. Each time the server updates
     *     the holdings of a client, that client's key is changed and the new key is sent
     *     to the client. If a client's key is out of date, that means the client holding
     *     information may be out of date. Orders received with out of date keys are rejected
     *  2) Market locking ensures that any market is only accessed by one transaction at a
     *     time. Orders that are waiting to access the market are kept in a backlog. When
     *     the backlog reaches a critical level, all orders are rejected until the server
     *     "catches up" sufficiently
     *
     *
     *
     */
    public UpdateBasket processOffer(int sessionId, AbstractOffer newOffer, long receivedKey) {
        int subjectId = newOffer.getSubjectId();
        int marketId = newOffer.getMarketId();
        int priceId = newOffer.getPriceId();
        int units = newOffer.getUnits();

        MarketState marketState = (MarketState) marketStates.get(new Integer(sessionId));
        if (marketState == null) {
            log.warn("Cannot process trade for session " + sessionId + " -- that session is not active!");
            return null;
        }

        int numOffers = incNumOffers(marketState, newOffer.getSubjectId());
        newOffer.setTime(getTimeElapsed(marketState));
        newOffer.setSubjectId_db(marketState.getDatabaseId(subjectId));

        String offerCode = "(S: " + subjectId + ", M: " + marketId + ", P: " + priceId + ", U: " + units + ", N: "
                + numOffers + ", R: " + rand.nextInt(4000) + ")";
        log.debug(offerCode + ": Received new offer");

        UpdateBasket basket = new UpdateBasket(offerCode);
        basket.addNumOffersUpdate(new NumOffersUpdate(sessionId, subjectId, numOffers));

        boolean valid = validateOffer(marketState, newOffer, receivedKey, offerCode);

        if (valid) {
            long newKey = (new java.util.Date()).getTime();

            marketState.setKey(subjectId, newKey);
            marketState.setKeyCode(subjectId, offerCode);

            lockMarket(marketState, marketId, offerCode);

            while (!newOffer.isCompleted()) {
                Connection conn = null;
                TransactionState transState = null;
                try {
                    conn = getConnection();
                    transState = new TransactionState(newOffer, offerCode, conn);

                    startTransaction(marketState, transState);
                    log.debug(offerCode + ": Transaction started");

                    if (newOffer.getAction() == JMConstants.CANCEL_ACTION)
                        basket = processCancel(marketState, transState, basket);
                    else
                        basket = checkForTransaction(marketState, transState, basket);

                    commit(marketState, transState, basket);
                    log.debug(offerCode + ": Transaction committed");

                    newOffer.finishedProcessing();

                } catch (TransactionInterruptedException e) {
                    log.warn("A transaction has been interrupted -- rolling back database and TradeServ");
                    basket = rollback(marketState, transState, basket);
                    newOffer.finishedProcessing();

                    if (e.isDeadlock()) {
                        log.warn("Retrying a deadlocked transaction");
                        return processOffer(sessionId, newOffer, receivedKey);
                    }

                    break;
                } finally {
                    dbw.closeConnection(conn);
                }
            }

            unlockMarket(marketState, marketId);
        }

        else {
            TradeUpdate invalid = new TradeUpdate(newOffer.getSubjectId_db(), TradeUpdate.INVALID_OFFER_UPDATE);
            invalid.setErrorMsg(newOffer.getInvalidMessage());
            basket.addTradeUpdate(invalid);
        }

        return basket;
    }

    /** Log a detailed description of a newly received offer */
    private void logOffer(MarketState marketState, AbstractOffer newOffer) {
        String good = marketState.getMarketInfo().getMarketTitles()[newOffer.getMarketId()];
        newOffer.setPrice(marketState.getMarketInfo().getPrices()[newOffer.getMarketId()][newOffer.getPriceId()]);

        if (newOffer.getAction() == JMConstants.BUY_ACTION)
            log.debug("Received bid offer at period time " + newOffer.getTime() + " by client ID: "
                    + newOffer.getSubjectId() + " for " + newOffer.getUnits() + " units of good '" + good
                    + "' at price: " + formatter.format(newOffer.getPrice()));
        if (newOffer.getAction() == JMConstants.SELL_ACTION)
            log.debug("Received ask offer at period time " + newOffer.getTime() + " by client ID: "
                    + newOffer.getSubjectId() + " for " + newOffer.getUnits() + " units of good '" + good
                    + "' at price: " + formatter.format(newOffer.getPrice()));
        if (newOffer.getAction() == JMConstants.CANCEL_ACTION)
            log.debug("Received cancel action at period time " + newOffer.getTime() + " buy client ID: "
                    + newOffer.getSubjectId() + " for " + newOffer.getUnits() + " units of good '" + good
                    + "' at price: " + formatter.format(newOffer.getPrice()));
    }

    /** Given a valid cancel order, go through each offer that is subject to cancellation by this order.
     *  Cancel all offers by this subject at this price level until the cancel order is exhausted. If an
     *  order must be cut in half (i.e. half its units are canceled) then just update the order with the
     *  new amount of units */
    private UpdateBasket processCancel(MarketState marketState, TransactionState transState, UpdateBasket basket)
            throws TransactionInterruptedException {
        Connection conn = null;
        Object[] results = null;
        try {
            MarketDef marketInfo = marketState.getMarketInfo();
            SubjectDef subjectInfo = marketState.getSubjectInfo();
            AbstractOffer offer = transState.getOffer();
            conn = transState.getConn();

            String good = marketInfo.getMarketTitles()[offer.getMarketId()];
            results = dbw.getOffersForCancel(offer.getSubjectId_db(), offer.getMarketId(), offer.getPriceId(),
                    marketInfo, conn);

            //Go through each of the cancelable offers
            while (true) {
                AbstractOffer standingOffer = dbw.getNextOffer(offer.getMarketId(), results, subjectInfo,
                        marketInfo, conn);
                if (standingOffer == null)
                    break;

                int unitsCanceled = executeCancel(marketState, transState, basket, standingOffer);
                offer.setUnits(offer.getUnits() - unitsCanceled);

                if (offer.getUnits() <= 0)
                    break;
            }

            transState.setIterations(-1);
            return basket;

        } catch (Exception e) {
            log.error("Failed to check for cancellations upon receipt of cancel order", e);
            throw new TransactionInterruptedException();
        } finally {
            dbw.closeOffers(results);
        }
    }

    /** Given bid/ask information, check the offer book to see if any transaction can be made */
    private UpdateBasket checkForTransaction(MarketState marketState, TransactionState transState,
            UpdateBasket basket) throws TransactionInterruptedException {
        try {
            int sessionId = marketState.getSessionId();
            int periodNum = marketState.getPeriodNum();
            MarketDef marketInfo = marketState.getMarketInfo();
            AbstractOffer newOffer = transState.getOffer();
            Connection conn = transState.getConn();
            Object[] results = null;

            if (newOffer.getAction() == JMConstants.BUY_ACTION)
                results = dbw.getAskOffers(sessionId, periodNum, newOffer.getMarketId(), newOffer.getPriceId(),
                        newOffer.getPrice(), marketInfo, conn);
            if (newOffer.getAction() == JMConstants.SELL_ACTION)
                results = dbw.getBidOffers(sessionId, periodNum, newOffer.getMarketId(), newOffer.getPriceId(),
                        newOffer.getPrice(), marketInfo, conn);

            return processTransactions(marketState, transState, results, basket);
        } catch (Exception e) {
            log.error("Failed to check for transactions upon receipt of new order", e);
            throw new TransactionInterruptedException();
        }
    }

    /** Iterate through the list of possible transactions contained in the ResultSet and
     *  process each one of them. Return an UpdateBasket object containing the updates that
     *  must be sent to the client upon commit. This object also contains the number of iterations
     *  processed in the trade (that is, the number of interactions between offers) */
    private UpdateBasket processTransactions(MarketState marketState, TransactionState transState, Object[] results,
            UpdateBasket basket) throws TransactionInterruptedException {
        Connection conn = null;
        try {
            long[] keys = marketState.getKeys();
            String[] keyCodes = marketState.getKeyCodes();
            OfferBook offerBook = marketState.getOfferBook();
            MarketDef marketInfo = marketState.getMarketInfo();
            SubjectDef subjectInfo = marketState.getSubjectInfo();

            AbstractOffer newOffer = transState.getOffer();
            conn = transState.getConn();

            int iterations = 0;

            int unitsTransacted = 0;
            int transId = -1;
            int marketOrderId = -1;
            float txnPrice = 0;

            //Go through each of standing offers with which a transaction can be made with this new offer,
            //in order of best (for the new offer player) first
            while (true) {
                AbstractOffer standingOffer = dbw.getNextOffer(newOffer.getMarketId(), results, subjectInfo,
                        marketInfo, conn);
                if (standingOffer == null)
                    break;
                else if (transId == -1)
                    transId = openTransaction(newOffer.getTime(), conn);

                if (iterations == 0)
                    marketOrderId = writeMarketOrder(marketState, newOffer, conn);

                //Update the key of the client involved in the standing offer
                keys[standingOffer.getSubjectId()] = (new java.util.Date()).getTime();
                keyCodes[standingOffer.getSubjectId()] = transState.getCode();

                int unitsTraded = executeTrade(marketState, transState, basket, standingOffer, transId);
                txnPrice += unitsTraded * standingOffer.getPrice();

                newOffer.setUnits(newOffer.getUnits() - unitsTraded);
                unitsTransacted += unitsTraded;

                iterations++;

                if (newOffer.getUnits() <= 0)
                    break;
            }

            //If a transaction was made with the new offer, add it to the market_orders table and close it
            if (transId > -1 && marketOrderId > 0) {
                executeTrade(transId, marketOrderId, unitsTransacted, newOffer.getAction(), transState);
                closeTransaction(transId, unitsTransacted, txnPrice / unitsTransacted, conn);
            }

            //If there are units remaining in the new offer then write it to the database and show it to the rest of the clients
            if (newOffer.getUnits() > 0)
                writeOffer(marketState, transState, basket);

            //Otherwise visually execute it from the client who made it (since only this client had it highlighted)
            else {
                TradeUpdate tupdate = createSinglePostUpdate(marketState, newOffer.getSubjectId(),
                        JMConstants.VISUAL_EXECUTE_ACTION, newOffer, newOffer.getTime(),
                        offerBook.getPriceBook(newOffer.getMarketId(), newOffer.getPriceId()),
                        transState.getCode());
                basket.addTradeUpdate(tupdate);
                log.debug(transState.getCode()
                        + ": All units resulted in transaction -- nothing has been posted to offer book");
            }

            transState.setIterations(iterations);

            return basket;

        } catch (Exception e) {
            log.error("Failed to check for transactions upon receipt of new order", e);
            throw new TransactionInterruptedException();
        } finally {
            dbw.closeOffers(results);
        }
    }

    /** Write the given market order into the database */
    private int writeMarketOrder(MarketState marketState, AbstractOffer offer, Connection conn)
            throws TransactionInterruptedException {
        return dbw.writeOffer(offer.getSubjectId_db(), offer.getMarketId(), offer.getPriceId(), offer.getAction(),
                offer.getUnits(), JMConstants.ORDER_META_MARKET, offer.getTime(), marketState.getMarketInfo(),
                conn);
    }

    /** Write the given offer into the database, the server offerbook, and the client offerbooks. Return
     *  the number of units posted to the offer book */
    private int writeOffer(MarketState marketState, TransactionState transState, UpdateBasket basket)
            throws TransactionInterruptedException {
        OfferBook offerBook = marketState.getOfferBook();
        AbstractOffer offer = transState.getOffer();
        Connection conn = transState.getConn();
        UpdateInvert uinvert = null;

        if (offer.getAction() == JMConstants.BUY_ACTION)
            uinvert = offerBook.insertBuy(offer.getMarketId(), offer.getPriceId(), offer.getSubjectId(),
                    offer.getUnits());
        else if (offer.getAction() == JMConstants.SELL_ACTION)
            uinvert = offerBook.insertSell(offer.getMarketId(), offer.getPriceId(), offer.getSubjectId(),
                    offer.getUnits());

        log.debug(transState.getCode() + ": Server-side offer book updated: "
                + offerBook.getState(offer.getMarketId(), offer.getPriceId(), offer.getSubjectId()));
        transState.addUpdateInvert(uinvert);

        dbw.writeOffer(offer.getSubjectId_db(), offer.getMarketId(), offer.getPriceId(), offer.getAction(),
                offer.getUnits(), JMConstants.ORDER_META_LIMIT, offer.getTime(), marketState.getMarketInfo(), conn);

        TradeUpdate[] tupdates = createPostUpdates(marketState, offer.getAction(), offer, offer.getTime(),
                transState.getCode());
        for (int i = 0; i < tupdates.length; i++)
            basket.addTradeUpdate(tupdates[i]);

        log.debug(transState.getCode() + ": Remaining non-traded " + offer.getUnits()
                + " unit(s) posted to offer book");
        return offer.getUnits();
    }

    /** Given a cancelOffer (which contains just the number of units to cancel and the time received) and a
     *  standing offer, cancel the designated number of units from the standing offer. If the standing offer
     *  still has units remaining, then just update in the database offerbook instead of setting it to
     *  canceled status. Finally, update the server offerbook and the client offerbooks */
    private int executeCancel(MarketState marketState, TransactionState transState, UpdateBasket basket,
            AbstractOffer standingOffer) throws TransactionInterruptedException {
        OfferBook offerBook = marketState.getOfferBook();
        AbstractOffer cancelOffer = transState.getOffer();
        Connection conn = transState.getConn();

        int unitsCanceled = Math.min(cancelOffer.getUnits(), standingOffer.getUnits());
        boolean canceled = unitsCanceled == standingOffer.getUnits();

        UpdateInvert uinvert = offerBook.insertCancel(standingOffer.getMarketId(), standingOffer.getPriceId(),
                standingOffer.getSubjectId(), unitsCanceled);
        log.debug(transState.getCode() + ": Server-side offer book updated: " + offerBook
                .getState(standingOffer.getMarketId(), standingOffer.getPriceId(), standingOffer.getSubjectId()));
        transState.addUpdateInvert(uinvert);

        if (canceled)
            dbw.cancelOffer(standingOffer.getId_db(), cancelOffer.getTime(), unitsCanceled, conn);
        else
            dbw.updateOffer(standingOffer.getId_db(), standingOffer.getUnits() - unitsCanceled,
                    cancelOffer.getTime(), unitsCanceled, JMConstants.ORDER_CANCELLED, conn);

        TradeUpdate[] tupdates = createPostUpdates(marketState, JMConstants.CANCEL_ACTION, standingOffer,
                cancelOffer.getTime(), transState.getCode());
        for (int i = 0; i < tupdates.length; i++)
            basket.addTradeUpdate(tupdates[i]);

        log.debug(transState.getCode() + ": Completed cancellation of " + unitsCanceled + " units at price "
                + formatter.format(standingOffer.getPrice()) + " for player " + standingOffer.getSubjectId());
        return unitsCanceled;
    }

    private void executeTrade(int transId, int marketOrderId, int units, int orderType, TransactionState transState)
            throws TransactionInterruptedException {
        Connection conn = transState.getConn();
        dbw.writeTrade(transId, marketOrderId, units, orderType, conn);
    }

    /** Given two offers that are eligible for a trade, and the transaction id that they belong to, execute the trade.
     *  First calculate whether the standing offer will have units remaining after being transacted with the
     *  new offer. If it will, then update it in the database offerbook instead of setting it to executed status. Then
     *  construct a trade object -- which encapsulates the pre- and post- cash and security holdings for each client,
     *  and update the database with the transaction information. Also update the server offerbook, send trade updates
     *  to update the client offerbooks, and update the price chart */
    private int executeTrade(MarketState marketState, TransactionState transState, UpdateBasket basket,
            AbstractOffer standingOffer, int transId) throws TransactionInterruptedException {
        OfferBook offerBook = marketState.getOfferBook();
        MarketDef marketInfo = marketState.getMarketInfo();
        Trader[] traders = marketState.getTraders();

        int sessionId = marketState.getSessionId();
        int periodNum = marketState.getPeriodNum();

        AbstractOffer newOffer = transState.getOffer();
        Connection conn = transState.getConn();

        int unitsReduced = Math.min(standingOffer.getUnits(), newOffer.getUnits());
        boolean executed = unitsReduced == standingOffer.getUnits();

        UpdateInvert uinvert = offerBook.insertCancel(standingOffer.getMarketId(), standingOffer.getPriceId(),
                standingOffer.getSubjectId(), unitsReduced);
        log.debug(transState.getCode() + ": Server-side offer book updated: " + offerBook
                .getState(standingOffer.getMarketId(), standingOffer.getPriceId(), standingOffer.getSubjectId()));
        transState.addUpdateInvert(uinvert);

        if (executed)
            dbw.executeOffer(standingOffer.getId_db(), newOffer.getTime(), unitsReduced, conn);
        else
            dbw.updateOffer(standingOffer.getId_db(), standingOffer.getUnits() - unitsReduced, newOffer.getTime(),
                    unitsReduced, JMConstants.ORDER_TRANSACTED, conn);

        Trade trade = new Trade(standingOffer, newOffer, unitsReduced, transId);
        trade.setPreAskCash(traders[trade.getAskParty()].getCash());
        trade.setPreBidCash(traders[trade.getBidParty()].getCash());
        trade.setPreAskSec(traders[trade.getAskParty()].getHoldings(standingOffer.getMarketId()));
        trade.setPreBidSec(traders[trade.getBidParty()].getHoldings(standingOffer.getMarketId()));

        log.debug(transState.getCode() + ": Starting trade execution with (Bidder: " + trade.getBidParty()
                + ", Units: " + trade.getPreBidSec() + ") and (Seller: " + trade.getAskParty() + ", Units: "
                + trade.getPreAskSec() + ")");

        trade = dbw.writeTrade(sessionId, periodNum, trade, marketInfo, conn);

        log.debug(transState.getCode() + ": Finished trade execution with (Bidder: " + trade.getBidParty()
                + ", Units: " + trade.getPostBidSec() + ") and (Seller: " + trade.getAskParty() + ", Units: "
                + trade.getPostAskSec() + ")");

        updateTraderData(traders, trade);
        TradeUpdate[] tupdates = createTradeUpdates(marketState, trade, transState.getCode());
        for (int i = 0; i < tupdates.length; i++)
            basket.addTradeUpdate(tupdates[i]);

        String good = marketInfo.getMarketTitles()[newOffer.getMarketId()];
        PriceChartUpdate pupdate = updatePriceChart(marketState, good, newOffer.getTime(),
                standingOffer.getPrice());
        basket.addPriceChartUpdate(pupdate);

        log.debug(transState.getCode() + ": Completed transaction of " + unitsReduced + " units at price "
                + formatter.format(standingOffer.getPrice()) + " between players " + newOffer.getSubjectId()
                + " and " + standingOffer.getSubjectId());
        return unitsReduced;
    }

    /** Validate the offer by checking if the key sent by the client is up to date */
    private boolean validateOffer(MarketState marketState, AbstractOffer offer, long key, String code) {
        long[] keys = marketState.getKeys();
        String[] keyCodes = marketState.getKeyCodes();

        if (backlogRejection) {
            offer.setInvalidMessage("Order canceled because server is busy -- please wait a few seconds");
            log.debug("TradeServ rejected offer, code " + code + ", because of excessive offer backlog");

            return false;
        }

        else if (keys[offer.getSubjectId()] != key) {
            offer.setInvalidMessage("Order canceled because you are currently involved in a transaction");
            log.debug("TradeServ rejected offer, code " + code
                    + ", because of antiquated key: last updated key-code: " + keyCodes[offer.getSubjectId()]
                    + ", client key: " + key + ", server key: " + keys[offer.getSubjectId()]);

            return false;
        }

        return true;
    }

    /** Get a transaction ID for the given offer. This is called when we know that a new offer has intersected
     *  with a standing offer such that at lesat one transaction will occur. All exchanges that occur with this
     *  new offer will be assigned this same transaction ID */
    private int openTransaction(long time, Connection conn) throws TransactionInterruptedException {
        return dbw.openTransaction(time, conn);
    }

    /** Close the given transaction, filling in the number of units in total transacted */
    private void closeTransaction(int transId, int units, float txnPrice, Connection conn)
            throws TransactionInterruptedException {
        dbw.closeTransaction(transId, units, txnPrice, conn);
    }

    /** Update the traders with the given transaction information */
    private void updateTraderData(Trader[] traders, Trade trade) {
        traders[trade.getBidParty()].setHoldings(trade.getMarketId(), trade.getPostBidSec());
        traders[trade.getBidParty()].setCash(trade.getPostBidCash());
        traders[trade.getBidParty()].incNumPurchases(trade.getMarketId(), trade.getAskParty(),
                trade.getUnitsTraded());

        traders[trade.getAskParty()].setHoldings(trade.getMarketId(), trade.getPostAskSec());
        traders[trade.getAskParty()].setCash(trade.getPostAskCash());
        traders[trade.getAskParty()].incNumSales(trade.getMarketId(), trade.getBidParty(), trade.getUnitsTraded());
    }

    /** Update the server-side copy of the price chart with the given transaction */
    private PriceChartUpdate updatePriceChart(MarketState marketState, String security, float time, float price) {
        try {
            int sessionId = marketState.getSessionId();
            PriceChart chart = marketState.getChart();
            chart.addPoint(security, time / 1000, price);

            PriceChartUpdate pupdate = new PriceChartUpdate(sessionId, security, time / 1000, price);
            return pupdate;
        } catch (Exception e) {
            log.error("Failed to update the price chart in the TradeServ", e);
        }
        return null;
    }

    /** Reset the given client's key to zero */
    public void resetClientKey(int sessionId, int subjectId) {
        MarketState marketState = (MarketState) marketStates.get(new Integer(sessionId));
        if (marketState == null) {
            log.error("Failed to reset client key for session " + sessionId + " -- that session does not exist!");
            return;
        }

        marketState.setKey(subjectId, 0);
    }

    /** Generate the client-side offer-book for the given client using the server-side offer-book */
    public OfferBook generateClientBook(int sessionId) {
        MarketState marketState = (MarketState) marketStates.get(new Integer(sessionId));
        if (marketState == null) {
            log.error("Failed to generate TradeServ information for session " + sessionId
                    + " -- that session does not exist!");
            return null;
        }

        return marketState.getOfferBook();
    }

    /** Generate the client-side holdings array for the given client */
    public int[] generateClientHoldings(int sessionId, int subjectId) {
        MarketState marketState = (MarketState) marketStates.get(new Integer(sessionId));
        if (marketState == null) {
            log.error("Failed to generate TradeServ information for session " + sessionId
                    + " -- that session does not exist!");
            return null;
        }

        return marketState.getTraders()[subjectId].getHoldings();
    }

    /** Generate the cash holdings of the given client */
    public float generateClientCash(int sessionId, int subjectId) {
        MarketState marketState = (MarketState) marketStates.get(new Integer(sessionId));
        if (marketState == null) {
            log.error("Failed to generate TradeServ information for session " + sessionId
                    + " -- that session does not exist!");
            return 0;
        }

        return marketState.getTraders()[subjectId].getCash();
    }

    /** Generate the securities view of the price chart for the given session */
    public Vector getPriceChartView(int sessionId) {
        MarketState marketState = (MarketState) marketStates.get(new Integer(sessionId));
        if (marketState == null) {
            log.info("No market information generated for new admin monitor in session " + sessionId
                    + " -- no market exists yet for the session");
            return null;
        }

        return marketState.getChartSecurities();
    }

    /** Get a database connection from the pool */
    private Connection getConnection() {
        return dbw.getConnection();
    }

    /**  Lock the market being used by this transaction, first waiting for it to become
     *  available. Implements a threading queue (FIFO) so that orders are processed in the
     *  order received. This is accomplished by adding each thread to the queue the FIRST
     *  time they attempt to get the monitor (the IF time). Each subsequent time (the WHILE
     *  attempts) we will check to see if they are first in the queue; if not, they wait
     *  again as the queue is updated */
    private synchronized void lockMarket(MarketState marketState, int marketId, String offerCode) {
        try {
            Thread offerThr = Thread.currentThread();
            boolean[] marketLocks = marketState.getMarketLocks();
            Vector[] threadQueue = marketState.getThreadQueue();

            if (marketLocks[marketId]) {
                threadQueue[marketId].add(offerThr);

                while (marketLocks[marketId] || threadQueue[marketId].get(0) != offerThr) {
                    if (marketLocks[marketId])
                        log.debug(offerCode + ": Market " + marketId + " locked -- waiting for release at position "
                                + threadQueue[marketId].indexOf(offerThr) + " in queue...");
                    wait();
                }

                threadQueue[marketId].remove(offerThr);
            }

            marketLocks[marketId] = true;
            log.debug(offerCode + ": Locked market");
        } catch (Exception e) {
            log.error("Failed to lock market " + marketId, e);
        }
    }

    /** Unlock the hold on the given market so that other threads can access it */
    private synchronized void unlockMarket(MarketState marketState, int marketId) {
        boolean[] marketLocks = marketState.getMarketLocks();

        marketLocks[marketId] = false;
        notifyAll();
    }

    /** Start a database transaction. Starts storing the client-side updates in a
     *  UpdateBasket, for release upon commit. Stamp the current time so that
     *  when committed we can see how long the transaction took to complete. Also
     *  stamp the current holdings of each client so that these can be recalled if
     *  rollback occurs */
    private TransactionState startTransaction(MarketState marketState, TransactionState transState) {
        try {
            Trader[] traders = marketState.getTraders();

            Connection conn = transState.getConn();
            int marketId = transState.getMarketId();
            int numClients = marketState.getNumClients();

            dbw.startTransaction(conn);

            transState.stampStartTime();

            float[] priorCashHoldings = new float[numClients];
            for (int i = 0; i < priorCashHoldings.length; i++)
                priorCashHoldings[i] = traders[i].getCash();

            int[] priorSecHoldings = new int[numClients];
            for (int i = 0; i < priorSecHoldings.length; i++)
                priorSecHoldings[i] = traders[i].getHoldings(marketId);

            int[][] priorPurchases = new int[numClients][numClients];
            for (int i = 0; i < priorPurchases.length; i++)
                priorPurchases[i] = traders[i].getNumPurchases(marketId);

            int[][] priorSales = new int[numClients][numClients];
            for (int i = 0; i < priorSales.length; i++)
                priorSales[i] = traders[i].getNumSales(marketId);

            transState.stampHoldings(priorCashHoldings, priorSecHoldings);
            transState.setPriorPurchases(priorPurchases);
            transState.setPriorSales(priorSales);

            return transState;
        } catch (Exception e) {
            log.error("Failed to start transaction", e);
            return transState;
        }
    }

    /** Commit the current database transaction. Sends the stored updates to the
     *  clients and record the time elapsed during the transaction. Release the lock
     *  on the market used by this transaction */
    private void commit(MarketState marketState, TransactionState transState, UpdateBasket basket)
            throws TransactionInterruptedException {
        String offerCode = transState.getCode();
        Connection conn = transState.getConn();

        log.debug(offerCode + ": Writing transaction to database...");

        dbw.commit(conn);

        log.debug(offerCode + ": Finished writing");

        transState.stampEndTime();

        MetricsUpdate mupdate = updateMetrics(marketState, transState);
        if (mupdate != null)
            basket.addMetricsUpdate(mupdate);

        OfferBacklogUpdate oupdate = updateBacklog();
        if (oupdate != null)
            basket.addOfferBacklogUpdate(oupdate);
    }

    /** Rollback all changes made in the current transaction. Dump the updates that
     *  were going to sent to the clients, rollback the server-side offerbook, rollback
     *  the database, and rollback the server-side holdings. Return the rolled back
     *  Update Basket */
    private UpdateBasket rollback(MarketState marketState, TransactionState transState, UpdateBasket basket) {
        OfferBook offerBook = marketState.getOfferBook();
        int subjectId_db = transState.getOffer().getSubjectId_db();
        int marketId = transState.getMarketId();
        Connection conn = transState.getConn();

        basket.getTradeUpdates().clear();
        basket.getMetricsUpdates().clear();
        basket.getNumOffersUpdates().clear();
        basket.getPriceChartUpdates().clear();
        basket.getTradeUpdates().clear();

        TradeUpdate invalid = new TradeUpdate(subjectId_db, TradeUpdate.INVALID_OFFER_UPDATE);
        invalid.setErrorMsg("Transaction failed for unknown reason -- please try again");
        basket.addTradeUpdate(invalid);

        Vector inverts = transState.getUpdateInverts();
        for (int i = 0; i < inverts.size(); i++) {
            UpdateInvert invert = (UpdateInvert) inverts.get(i);
            offerBook.invertUpdate(invert);
            log.debug(transState.getCode() + ": Server-side offer book ROLLED BACK: "
                    + offerBook.getState(invert.market, invert.price, invert.subject));
        }

        Trader[] traders = marketState.getTraders();
        float[] priorCashHoldings = transState.getPriorCashHoldings();
        int[] priorSecHoldings = transState.getPriorSecHoldings();
        int[][] priorPurchases = transState.getPriorPurchases();
        int[][] priorSales = transState.getPriorSales();

        for (int i = 0; i < traders.length; i++) {
            traders[i].setCash(priorCashHoldings[i]);
            traders[i].setHoldings(marketId, priorSecHoldings[i]);
            traders[i].setNumPurchases(marketId, priorPurchases[i]);
            traders[i].setNumSales(marketId, priorSales[i]);
        }
        dbw.rollback(conn);

        return basket;
    }

    /** Given the number of iterations of a transaction, return a MetricsUpdate object
     *  with the new average transaction times */
    private MetricsUpdate updateMetrics(MarketState marketState, TransactionState transState) {
        int iterations = transState.getIterations();
        long time = transState.getTimeElapsed();
        int[] numTrans = marketState.getNumTrans();
        long[] transTime = marketState.getTransTime();

        if (iterations >= MAX_RECORDED_ITERATIONS || iterations < 0)
            return null;

        numTrans[iterations]++;
        transTime[iterations] += time;

        float average = transTime[iterations] / numTrans[iterations];
        log.debug("The average transaction time for " + iterations + " iterations is " + average);

        MetricsUpdate update = new MetricsUpdate(marketState.getSessionId(), iterations, numTrans[iterations],
                average);
        return update;
    }

    /** Increment the number of offers attempted by the given client, and add those to
     *  the numOffersUpdates array */
    private int incNumOffers(MarketState marketState, int client) {
        return marketState.incNumOffers(client);
    }

    /** Return an OfferBacklogUpdate that contains the new number of offers in the offer backlog.
     *  If the number of offers in the backlog exceeds BACKLOG_MAX_LEVEL, then reject all
     *  incoming offers until the number drops to BACKLOG_RESTART_LEVEL. This rejection is accomplished
     *  in the validate function */
    private OfferBacklogUpdate updateBacklog() {
        int backlog = 0;

        Enumeration activeMarkets = marketStates.elements();
        while (activeMarkets.hasMoreElements()) {
            MarketState marketState = (MarketState) activeMarkets.nextElement();
            Vector[] threadQueue = marketState.getThreadQueue();

            for (int i = 0; i < threadQueue.length; i++)
                backlog += threadQueue[i].size();
        }

        if (backlog > JMConstants.BACKLOG_MAX_LEVEL) {
            if (!backlogRejection)
                log.info("Server backlog reached critical level " + backlog
                        + "; rejecting all offers until backlog reaches " + JMConstants.BACKLOG_RESTART_LEVEL);
            backlogRejection = true;
        } else if (backlog <= JMConstants.BACKLOG_RESTART_LEVEL) {
            if (backlogRejection)
                log.info("Server has reached backlog level " + backlog + "; now accepting offers");
            backlogRejection = false;
        }

        OfferBacklogUpdate update = new OfferBacklogUpdate(backlog, backlogRejection);
        return update;
    }

    /** Gets the time that has elapsed since the last period started, in milliseconds */
    private long getTimeElapsed(MarketState marketState) {
        java.util.Date date = new java.util.Date();
        return date.getTime() - marketState.getPeriodStartTime();
    }

    /** Create OFFER_BOOK_UPDATES for every player in response to the given trade. The update contains
     *  a pricebook array that contains the updated data on bids and asks for the market and price level
     *  on which the given trade took place. The EXECUTE action tells the client to perform a gold flickering.
     *  For clients involved in the trade, updated cash and security holdings are sent */
    private TradeUpdate[] createTradeUpdates(MarketState marketState, Trade trade, String code) {
        OfferBook offerBook = marketState.getOfferBook();
        Trader[] traders = marketState.getTraders();

        int bidParty = trade.getBidParty();
        int askParty = trade.getAskParty();

        int action = JMConstants.EXECUTE_ACTION;
        long time = trade.getNewOffer().getTime();
        int marketId = trade.getMarketId();
        int priceId = trade.getStandingOffer().getPriceId();
        int[] priceBook = offerBook.getPriceBook(marketId, priceId);

        int periodNum = marketState.getPeriodNum();
        int numClients = marketState.getNumClients();
        long[] keys = marketState.getKeys();

        TradeUpdate[] tupdates = new TradeUpdate[numClients];

        for (int i = 0; i < numClients; i++) {
            int dbId = marketState.getDatabaseId(i);

            TradeUpdate tupdate = new TradeUpdate(dbId, TradeUpdate.NON_TRANSACTION_UPDATE);

            tupdate.setAction(action);
            tupdate.setMarketId(marketId);
            tupdate.setExecutedPriceId(priceId);
            tupdate.setStandingPriceId(priceId);
            tupdate.setTime(time);
            tupdate.setKey(keys[i]);
            tupdate.setCode(code);
            tupdate.setPriceBook(priceBook);
            tupdate.setPeriodNum(periodNum);

            if (i == bidParty || i == askParty) {
                tupdate.setType(TradeUpdate.TRANSACTION_UPDATE);

                if (i == bidParty) {
                    tupdate.setCashHoldings(trade.getPostBidCash());
                    tupdate.setSecurityHoldings(trade.getPostBidSec());
                    tupdate.setTxnActionType("Buy");
                } else {
                    tupdate.setCashHoldings(trade.getPostAskCash());
                    tupdate.setSecurityHoldings(trade.getPostAskSec());
                    tupdate.setTxnActionType("Sell");
                }

                tupdate.setNumPurchases(traders[i].getNumPurchases()[marketId]);
                tupdate.setNumSales(traders[i].getNumSales()[marketId]);
                tupdate.setNumUnitsInTrade(trade.getUnitsTraded());
            }

            tupdates[i] = tupdate;
        }
        return tupdates;
    }

    /** Create an OFFER_BOOK_UPDATE for every player. This will tell each player the total number
     *  of asks and bids on the given price level specified by the marketId and priceId. In other words,
     *  tell each client to update their offerbooks with the given offer. Store the update in a
     *  TradeUpdate object so that it will be sent to the clients upon commit */
    private TradeUpdate[] createPostUpdates(MarketState marketState, int action, AbstractOffer offer, long time,
            String code) {
        int numClients = marketState.getNumClients();
        OfferBook offerBook = marketState.getOfferBook();

        TradeUpdate[] tupdates = new TradeUpdate[numClients];
        int[] priceBook = offerBook.getPriceBook(offer.getMarketId(), offer.getPriceId());

        for (int i = 0; i < numClients; i++) {
            tupdates[i] = createSinglePostUpdate(marketState, i, action, offer, time, priceBook, code);
        }

        return tupdates;
    }

    /** Create an update to send the given client telling her to post the given offer to her offerbook.
     *  See comments under createPostUpdate above */
    private TradeUpdate createSinglePostUpdate(MarketState marketState, int subjectId, int action,
            AbstractOffer offer, long time, int[] priceBook, String code) {
        int marketId = offer.getMarketId();
        int priceId = offer.getPriceId();
        long[] keys = marketState.getKeys();
        int periodNum = marketState.getPeriodNum();
        int subjectId_db = marketState.getDatabaseId(subjectId);

        TradeUpdate tupdate = new TradeUpdate(subjectId_db, TradeUpdate.NON_TRANSACTION_UPDATE);

        tupdate.setAction(action);
        tupdate.setMarketId(marketId);
        tupdate.setExecutedPriceId(priceId);
        tupdate.setStandingPriceId(priceId);
        tupdate.setTime(time);
        tupdate.setKey(keys[subjectId]);
        tupdate.setCode(code);
        tupdate.setPriceBook(priceBook);
        tupdate.setPeriodNum(periodNum);

        return tupdate;
    }

    /** Database access writer */
    private DBWriter dbw;

    /** Contains the market state objects for each active session, keyed by session Id */
    private Hashtable marketStates;

    /** A random number generator */
    private Random rand;

    /** True if we are currently in offer rejection state because of an excessive backlog. This
     *  will not be set to false until the backlog drops to BACKLOG_RESTART_LEVEL */
    private boolean backlogRejection;

    /** A simple number formatter */
    private NumberFormat formatter;

    /** The max number of iterations that will have metrics recorded */
    private int MAX_RECORDED_ITERATIONS = 5;

    private static Log log = LogFactory.getLog(ContinuousMarketEngine.class);

    public UpdateBasket processClosePeriod(int sessionId) {
        return null;
    }

}