/*************************************************************************** * Copyright (C) 2012 by H-Store Project * * Brown University * * Massachusetts Institute of Technology * * Yale University * * * * Alex Kalinin (akalinin@cs.brown.edu) * * http://www.cs.brown.edu/~akalinin/ * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be * * included in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * * OTHER DEALINGS IN THE SOFTWARE. * ***************************************************************************/ package edu.brown.benchmark.tpce.generators; import java.util.ArrayList; import java.util.Comparator; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.PriorityQueue; import org.voltdb.catalog.Database; import org.voltdb.types.TimestampType; import org.voltdb.utils.NotImplementedException; import edu.brown.benchmark.tpce.TPCEConstants; import edu.brown.benchmark.tpce.generators.CustomerAccountsGenerator.TaxStatus; import edu.brown.benchmark.tpce.generators.CustomerSelection.TierId; import edu.brown.benchmark.tpce.generators.StatusTypeGenerator.StatusTypeId; import edu.brown.benchmark.tpce.generators.TPCEGenerator.InputFile; import edu.brown.benchmark.tpce.util.EGenDate; import edu.brown.benchmark.tpce.util.EGenMoney; import edu.brown.benchmark.tpce.util.EGenRandom; /** * Note that this class generates all the growing tables. Since they depend on the same * process of generating trades, it would be hard to separate them and, moreover, * very inefficient. * * next() and hasNext() method work as expected, but the caller should check the table for * which next() just gave the next tuple. Tuples from different tables can be intermixed. So, * hasNext() should be read as "we have another tuple for one of the growing tables". * * @author akalinin */ public class TradeGenerator implements Iterator<Object[]> { public enum TradeType { eMarketBuy, eMarketSell, eStopLoss, eLimitSell, eLimitBuy; } private class TradeInfo { public long tradeId; // customer that executes the trade public long customer; public TierId customerTier; public long accId; // account for the trade public long secFileIndex; // symbol record from the security file public int secAccIndex; // index of the symbol from the account basket public TradeType tradeType; public StatusTypeId tradeStatus; public EGenMoney bidPrice; public EGenMoney tradePrice; public int tradeQty; // All times are in seconds from the start time public double completionTime; public double submissionTime; public double pendingTime; // only for limit buys/sells public boolean isLIFO; } private class HoldingInfo { public long tradeId; public int tradeQty; EGenMoney tradePrice; Date buyDTS; // absolute time (i.e., EGenDate here) long symbolIndex; // stock symbol index in the input flat file - stored for performance } private class AddTradeInfo { public EGenMoney buyValue; public EGenMoney sellValue; public EGenMoney tax; public long brokerId; EGenMoney charge; EGenMoney commission; EGenMoney settlement; public TaxStatus taxStatus; boolean isCash; } /* * Since we load a lot of different table via this generator, * this enum describes different phases of generating tuples. * * This is basically the current table we are generating a tuple for. */ private enum State { stNone, stTrade, stTradeHistory, stSettle, stCash, stHoldHistory, stBroker, stHoldSummary, stHolding } private final static double MEAN_IN_THE_MONEY_SUBMISSION_DELAY = 1.0; // Percentage of trades modifying holdings in Last-In-First-Out order. private static final int PERCENT_TRADE_IS_LIFO = 35; /* * Maximum number of HOLDING_HISTORY rows that can be output * for one completed trade. * * Determined by the maximum number of holdings that a trade * can modify. The maximum number would be a trade with * the biggest possible quantity modifying holdings each having * the smallest possible quantity. */ private static final int MAX_HOLDING_HISTORY_ROWS_PER_TRADE = 800 / 100; // Number of RNG calls for one simulated trade private static final int RNG_SKIP_ONE_TRADE = 11; // average count for v3.5: 6.5 // percent of cash transactions private static final int PERCENT_BUYS_ON_MARGIN = 16; // number of accounts per load unit private static final int LOAD_UNIT_ACCOUNT_COUNT = TPCEConstants.DEFAULT_LOAD_UNIT * CustomerAccountsGenerator.MAX_ACCOUNTS_PER_CUST; /* * this lists hold pre-generated holding/trade history rows * the corresponding counters iterate through them in next() */ private final List<Object[]> holdingHistory = new ArrayList<Object[]>(); private final List<Object[]> tradeHistory = new ArrayList<Object[]>(); private int holdHistoryCounter; private int tradeHistoryCounter; private final long totalTrades; private final int tradesPerWorkDay; private final double meanBetweenTrades; private boolean hasNextTrade; // if we can generate another trade after the current one private boolean hasNextHoldingSummary; private boolean hasNextHolding; private int currentLoadUnit = -1; private final int totalLoadUnits; private String currTable; private State currState = State.stNone; private final EGenRandom rnd = new EGenRandom(EGenRandom.RNG_SEED_TRADE_GEN); private final CustomerSelection custSelection; private final HoldingsAndTrades holdsGenerator; private final MEESecurity meeSecurity; private final CustomerAccountsGenerator custAccGenerator; private final AddressGenerator addrGenerator; private final CustomerTaxRatesGenerator custTaxGenerator; private final SecurityGenerator secGenerator; private final BrokerGenerator brokerGenerator; private final InputFileHandler chargeFile; private final InputFileHandler tradeTypeFile; private final InputFileHandler commissionRateFile; private final InputFileHandler statusTypeFile; private final SecurityHandler secHandler; private final PersonHandler personHandler; private long currCompletedTrades, currInitiatedTrades; private TradeInfo newTrade; private final AddTradeInfo addTrade = new AddTradeInfo(); // generated once for every trade row //private List<List<HoldingInfo>> customerHoldings = new ArrayList<List<HoldingInfo>>(); private final List<HoldingInfo>[][] customerHoldings; private int holdingIterator; // it is not an iterator, just an index in the lisr pointed by (currAccountForHolding, currSecurityForHolding) private int currAccountForHolding; private int currSecurityForHolding; private int currAccountForHoldingSummary; private int currSecurityForHoldingSummary; private long startFromAccount; // starting account number for customers private long startFromCustomer; // starting customer number private double currentSimulatedTime; private Date startTime; private long currentTradeId; // trade priority queue and comparator private class TradeInfoComparator implements Comparator<TradeInfo> { @Override public int compare(TradeInfo t1, TradeInfo t2) { if (t1.completionTime < t2.completionTime) { return -1; } else if (t1.completionTime > t2.completionTime) { return 1; } else { return 0; } } } private final TradeInfoComparator tradeComparator = new TradeInfoComparator(); private PriorityQueue<TradeInfo> currentTrades = new PriorityQueue<TradeGenerator.TradeInfo>(11, tradeComparator); private final Database catalogDb; /** * @param catalog_tbl * @param generator */ public TradeGenerator(TPCEGenerator generator, Database catalogDb) { int scalingFactor = generator.getScalingFactor(); int hoursOfInitialTrades = generator.getInitTradeDays() * 8; // 8 hours per work day totalTrades = hoursOfInitialTrades * 3600 * TPCEConstants.DEFAULT_LOAD_UNIT / scalingFactor; // 8 hours per work day tradesPerWorkDay = 8 * 3600 * TPCEConstants.DEFAULT_LOAD_UNIT / scalingFactor * HoldingsAndTrades.ABORT_TRADE / 100; // 8 hours per work day, 3600 seconds per hour meanBetweenTrades = 100.0 / HoldingsAndTrades.ABORT_TRADE * (double)scalingFactor / TPCEConstants.DEFAULT_LOAD_UNIT; customerHoldings = (List<HoldingInfo>[][])new ArrayList[LOAD_UNIT_ACCOUNT_COUNT][HoldingsAndTrades.MAX_SECURITIES_PER_ACCOUNT]; for (int i = 0; i < LOAD_UNIT_ACCOUNT_COUNT; i++) { for (int j = 0; j < HoldingsAndTrades.MAX_SECURITIES_PER_ACCOUNT; j++) { customerHoldings[i][j] = new ArrayList<HoldingInfo>(); } } currentTradeId = hoursOfInitialTrades * 3600 * (generator.getStartCustomer() - TPCEConstants.DEFAULT_START_CUSTOMER_ID) / scalingFactor * HoldingsAndTrades.ABORT_TRADE / 100 + TPCEConstants.TRADE_SHIFT; custSelection = new CustomerSelection(rnd, 0, 0, 100, generator.getStartCustomer(), TPCEConstants.DEFAULT_LOAD_UNIT); holdsGenerator = new HoldingsAndTrades(generator); secGenerator = (SecurityGenerator)generator.getTableGen(TPCEConstants.TABLENAME_SECURITY, null); brokerGenerator = new BrokerGenerator(catalogDb.getTables().get(TPCEConstants.TABLENAME_BROKER), generator); meeSecurity = new MEESecurity(); startFromCustomer = generator.getStartCustomer() + TPCEConstants.IDENT_SHIFT; this.catalogDb = catalogDb; startTime = EGenDate.getDateFromTime(TPCEConstants.initialTradePopulationBaseYear, TPCEConstants.initialTradePopulationBaseMonth, TPCEConstants.initialTradePopulationBaseDay, TPCEConstants.initialTradePopulationBaseHour, TPCEConstants.initialTradePopulationBaseMinute, TPCEConstants.initialTradePopulationBaseSecond, TPCEConstants.initialTradePopulationBaseFraction); custAccGenerator = (CustomerAccountsGenerator) generator.getTableGen(TPCEConstants.TABLENAME_CUSTOMER_ACCOUNT, null); addrGenerator = (AddressGenerator) generator.getTableGen(TPCEConstants.TABLENAME_ADDRESS, null); custTaxGenerator = (CustomerTaxRatesGenerator) generator.getTableGen(TPCEConstants.TABLENAME_CUSTOMER_TAXRATE, null); chargeFile = generator.getInputFile(InputFile.CHARGE); tradeTypeFile = generator.getInputFile(InputFile.TRADETYPE); statusTypeFile = generator.getInputFile(InputFile.STATUS); commissionRateFile = generator.getInputFile(InputFile.COMMRATE); secHandler = new SecurityHandler(generator); personHandler = new PersonHandler(generator.getInputFile(InputFile.LNAME), generator.getInputFile(InputFile.FEMFNAME), generator.getInputFile(InputFile.MALEFNAME)); totalLoadUnits = (int)(generator.getCustomersNum() / TPCEConstants.DEFAULT_LOAD_UNIT); } private void initNextLoadUnit() { currCompletedTrades = 0; // empty customer holdings for (int i = 0; i < LOAD_UNIT_ACCOUNT_COUNT; i++) { for (int j = 0; j < HoldingsAndTrades.MAX_SECURITIES_PER_ACCOUNT; j++) { customerHoldings[i][j].clear(); } } currAccountForHolding = 0; currSecurityForHolding = 0; currAccountForHoldingSummary = 0; currSecurityForHoldingSummary = -1; // for findNextHoldingList() correctness if (currentLoadUnit > 0) { // second and further load units startFromCustomer += TPCEConstants.DEFAULT_LOAD_UNIT; } startFromAccount = CustomerAccountsGenerator.getStartingAccId(startFromCustomer); currentSimulatedTime = 0; currInitiatedTrades = 0; brokerGenerator.initNextLoadUnit(TPCEConstants.DEFAULT_LOAD_UNIT, startFromCustomer - TPCEConstants.IDENT_SHIFT); // rnd generator long skipCount = startFromCustomer / TPCEConstants.DEFAULT_LOAD_UNIT * totalTrades; rnd.setSeedNth(EGenRandom.RNG_SEED_TRADE_GEN, skipCount * RNG_SKIP_ONE_TRADE); holdsGenerator.initNextLoadUnit(skipCount); if (currentLoadUnit > 0) { // second and further load units custSelection.setPartitionRange(startFromCustomer, TPCEConstants.DEFAULT_LOAD_UNIT); } meeSecurity.init(0, null, null, MEAN_IN_THE_MONEY_SUBMISSION_DELAY); hasNextHoldingSummary = true; hasNextHolding = true; } private double generateDelay() { return rnd.doubleIncrRange(0.0, meanBetweenTrades - 0.001, 0.001); } private void generateNewTrade() { newTrade = new TradeInfo(); newTrade.tradeId = ++currentTradeId; Object[] custIdAndTier = custSelection.genRandomCustomer(); newTrade.customer = (Long)custIdAndTier[0]; newTrade.customerTier = (TierId)custIdAndTier[1]; long[] accIdAndSecs = holdsGenerator.generateRandomAccSecurity(newTrade.customer, newTrade.customerTier); newTrade.accId = accIdAndSecs[0]; newTrade.secAccIndex = (int)accIdAndSecs[1]; newTrade.secFileIndex = accIdAndSecs[2]; newTrade.tradeType = generateTradeType(); newTrade.tradeStatus = StatusTypeId.E_COMPLETED; // always "completed" for the loading phase newTrade.bidPrice = new EGenMoney(rnd.doubleIncrRange(TPCEConstants.minSecPrice, TPCEConstants.maxSecPrice, 0.01)); newTrade.tradeQty = HoldingsAndTrades.TRADE_QTY_SIZES[rnd.intRange(0, HoldingsAndTrades.TRADE_QTY_SIZES.length - 1)]; if (newTrade.tradeType == TradeType.eMarketBuy || newTrade.tradeType == TradeType.eMarketSell) { newTrade.submissionTime = currentSimulatedTime; newTrade.bidPrice = meeSecurity.calculatePrice(newTrade.secFileIndex, currentSimulatedTime); // correcting the price } else { // limit order newTrade.pendingTime = currentSimulatedTime; newTrade.submissionTime = meeSecurity.getSubmissionTime(newTrade.secFileIndex, newTrade.pendingTime, newTrade.bidPrice, newTrade.tradeType); /* * Move orders that would submit after market close (5pm) * to the beginning of the next day. * * Submission time here is kept from the beginning of the day, even though * it is later output to the database starting from 9am. So time 0h corresponds * to 9am, time 8hours corresponds to 5pm. * * -- The comment is from EGen */ if (((int)(newTrade.submissionTime / 3600) % 24 == 8) && // 24 hour day, 8 hours work days; >=5pm ((newTrade.submissionTime / 3600) - (int)(newTrade.submissionTime / 3600) > 0)) { // fractional seconds exist, e.g. not 5:00pm newTrade.submissionTime += 16 * 3600; // add 16 hours to move to 9am next day } } // get a completion time and a trade price Object[] completionTimeAndPrice = meeSecurity.getCompletionTimeAndPrice(newTrade.secFileIndex, newTrade.submissionTime); newTrade.completionTime = (Double)completionTimeAndPrice[0]; newTrade.tradePrice = (EGenMoney)completionTimeAndPrice[1]; // Make sure the trade has the right price based on the type of trade. if ((newTrade.tradeType == TradeType.eLimitBuy && newTrade.bidPrice.lessThan(newTrade.tradePrice)) || (newTrade.tradeType == TradeType.eLimitSell && newTrade.tradePrice.lessThan(newTrade.bidPrice))) { newTrade.tradePrice = newTrade.bidPrice; } // determine if the trade is a LIFO one if (rnd.rndPercent(PERCENT_TRADE_IS_LIFO)) { newTrade.isLIFO = true; } else { newTrade.isLIFO = false; } currInitiatedTrades++; } private TradeType generateTradeType() { TradeType res = TradeType.eLimitBuy; // to suppress the warning /* * Generate Trade Type * NOTE: The order of these "if" tests is significant! */ int loadTradeTypePct = rnd.rndPercentage(); if (loadTradeTypePct <= HoldingsAndTrades.MARKET_BUY_LOAD_THRESHOLD) { // 1% - 30% res = TradeType.eMarketBuy; } else if(loadTradeTypePct <= HoldingsAndTrades.MARKET_SELL_LOAD_THRESHOLD) { // 31% - 60% res = TradeType.eMarketSell; } else if(loadTradeTypePct <= HoldingsAndTrades.LIMIT_BUY_LOAD_THRESHOLD) { // 61% - 80% res = TradeType.eLimitBuy; } else if (loadTradeTypePct <= HoldingsAndTrades.LIMIT_SELL_LOAD_THRESHOLD) { // 81% - 90% res = TradeType.eLimitSell; } else if(loadTradeTypePct <= HoldingsAndTrades.STOP_LOSS_LOAD_THRESHOLD) { // 91% - 100% res = TradeType.eStopLoss; } else { assert false; // this should never happen } return res; } private void generateCompleteTrade() { generateCompletedTradeInfo(); // EGen functions generating rows were moved to next() // update broker info brokerGenerator.updateTradeAndCommissionYTD(addTrade.brokerId, 1, addTrade.commission.getDollars()); currCompletedTrades++; } private void updateHoldings() { addTrade.buyValue = new EGenMoney(0); addTrade.sellValue = new EGenMoney(0); int neededQty = newTrade.tradeQty; // new series of holding history rows holdingHistory.clear(); List<HoldingInfo> holdingList = getHoldingListForCurrentTrade(); int holdingIndex = positionAtHoldingList(holdingList, newTrade.isLIFO); if (newTrade.tradeType == TradeType.eMarketBuy || newTrade.tradeType == TradeType.eLimitBuy) { // Buy trade: liquidate negative (short) holdings while (!holdingList.isEmpty() && holdingList.get(holdingIndex).tradeQty < 0 && neededQty > 0) { HoldingInfo holding = holdingList.get(holdingIndex); int holdQty = holding.tradeQty; holding.tradeQty += neededQty; if (holding.tradeQty > 0) { holding.tradeQty = 0; // holding fully closed addTrade.sellValue.add(EGenMoney.mulMoneyByInt(holding.tradePrice, -holdQty)); addTrade.buyValue.add(EGenMoney.mulMoneyByInt(newTrade.tradePrice, -holdQty)); } else { addTrade.sellValue.add(EGenMoney.mulMoneyByInt(holding.tradePrice, neededQty)); addTrade.buyValue.add(EGenMoney.mulMoneyByInt(newTrade.tradePrice, neededQty)); } generateHoldingHistoryRow(holding.tradeId, newTrade.tradeId, holdQty, holding.tradeQty); if (holding.tradeQty == 0) { holdingList.remove(holdingIndex); } holdingIndex = positionAtHoldingList(holdingList, newTrade.isLIFO); neededQty += holdQty; } if (neededQty > 0) { // Still shares left after closing positions => create a new holding HoldingInfo newHolding = new HoldingInfo(); newHolding.tradeId = newTrade.tradeId; newHolding.tradeQty = neededQty; newHolding.tradePrice = newTrade.tradePrice; newHolding.buyDTS = getCurrentTradeCompletionTime(); newHolding.symbolIndex = newTrade.secFileIndex; holdingList.add(newHolding); generateHoldingHistoryRow(newTrade.tradeId, newTrade.tradeId, 0, neededQty); } } else { // Sell trade: liquidate positive (long) holdings while (!holdingList.isEmpty() && holdingList.get(holdingIndex).tradeQty > 0 && neededQty > 0) { HoldingInfo holding = holdingList.get(holdingIndex); int holdQty = holding.tradeQty; holding.tradeQty -= neededQty; if (holding.tradeQty < 0) { holding.tradeQty = 0; // holding fully closed addTrade.sellValue.add(EGenMoney.mulMoneyByInt(newTrade.tradePrice, holdQty)); addTrade.buyValue.add(EGenMoney.mulMoneyByInt(holding.tradePrice, holdQty)); } else { addTrade.sellValue.add(EGenMoney.mulMoneyByInt(newTrade.tradePrice, neededQty)); addTrade.buyValue.add(EGenMoney.mulMoneyByInt(holding.tradePrice, neededQty)); } generateHoldingHistoryRow(holding.tradeId, newTrade.tradeId, holdQty, holding.tradeQty); if (holding.tradeQty == 0) { holdingList.remove(holdingIndex); } holdingIndex = positionAtHoldingList(holdingList, newTrade.isLIFO); neededQty -= holdQty; } if (neededQty > 0) { // Still shares left after closing positions => create a new holding HoldingInfo newHolding = new HoldingInfo(); newHolding.tradeId = newTrade.tradeId; newHolding.tradeQty = -neededQty; newHolding.tradePrice = newTrade.tradePrice; newHolding.buyDTS = getCurrentTradeCompletionTime(); newHolding.symbolIndex = newTrade.secFileIndex; holdingList.add(newHolding); generateHoldingHistoryRow(newTrade.tradeId, newTrade.tradeId, 0, -neededQty); } } } /* * Helper function to get the list of holdings * to modify after the last completed trade * * RETURNS: * reference to the list of holdings */ private List<HoldingInfo> getHoldingListForCurrentTrade() { return customerHoldings[(int)(newTrade.accId - startFromAccount)][newTrade.secAccIndex - 1]; } private int positionAtHoldingList(List<HoldingInfo> holdingList, boolean isLifo) { if (holdingList.isEmpty()) { return holdingList.size(); // iterator positioned after the last element } else if (isLifo) { return holdingList.size() - 1; // position before the last element } else { return 0; } } private boolean findNextHolding() { List<HoldingInfo> holdList = customerHoldings[currAccountForHolding][currSecurityForHolding]; do { if (holdingIterator == holdList.size()) { // have to get a new list currSecurityForHolding++; if (currSecurityForHolding == HoldingsAndTrades.MAX_SECURITIES_PER_ACCOUNT) { // no more securities, so move the account counter currAccountForHolding++; currSecurityForHolding = 0; if (currAccountForHolding == LOAD_UNIT_ACCOUNT_COUNT) { // no more holdings return false; } } holdList = customerHoldings[currAccountForHolding][currSecurityForHolding]; holdingIterator = 0; } } while (holdingIterator == holdList.size()); return true; } private boolean findNextHoldingList() { List<HoldingInfo> holdList; do { currSecurityForHoldingSummary++; if (currSecurityForHoldingSummary == HoldingsAndTrades.MAX_SECURITIES_PER_ACCOUNT) { // no more securities, so move the account counter currAccountForHoldingSummary++; currSecurityForHoldingSummary = 0; if (currAccountForHoldingSummary == LOAD_UNIT_ACCOUNT_COUNT) { // no more holdings return false; } } holdList = customerHoldings[currAccountForHoldingSummary][currSecurityForHoldingSummary]; } while (holdList.isEmpty()); return true; } private void generateHoldingHistoryRow(long holdingTradeId, long tradeTradeId, int beforeQty, int afterQty) { if (holdingHistory.size() < MAX_HOLDING_HISTORY_ROWS_PER_TRADE) { int columnNum = getColumnNum(TPCEConstants.TABLENAME_HOLDING_HISTORY); Object[] tuple = new Object[columnNum]; tuple[0] = holdingTradeId; // hh_h_t_id tuple[1] = tradeTradeId; // hh_t_id tuple[2] = beforeQty; // hh_before_qty tuple[3] = afterQty; // hh_after_qty holdingHistory.add(tuple); } } private void generateTradeCharge() { for (int i = 0; i < chargeFile.getRecordsNum(); i++) { String[] chargeRow = chargeFile.getTupleByIndex(i); // [1] is the tier number if (Integer.valueOf(chargeRow[1]) == newTrade.customerTier.ordinal() + 1) { // pick the trade type row (we assume it is indexed on the enum // int values) String[] ttRow = tradeTypeFile.getTupleByIndex(newTrade.tradeType.ordinal()); // compare trade type symbols if (chargeRow[0].equals(ttRow[0])) { addTrade.charge = new EGenMoney(Double.valueOf(chargeRow[2])); return; } } } // should never reach this point assert false; } private void generateTradeCommission() { int tier = newTrade.customerTier.ordinal() + 1; int tradeQty = newTrade.tradeQty; int tradeType = newTrade.tradeType.ordinal(); /* * Some extra logic to reduce looping in the CommissionRate file. * It is organized by tier, then trade type, then exchange. * Consider this extra knowledge to calculate the starting position for search. */ //Number of rows in the CommissionRate file that have the same customer tier. int customerTierRecords = commissionRateFile.getRecordsNum() / 3; // Number of rows in the CommissionRate file per trade type. int tradeTypeRecords = customerTierRecords / tradeTypeFile.getRecordsNum(); for (int i = (tier - 1) * customerTierRecords + tradeType * tradeTypeRecords; i < commissionRateFile.getRecordsNum(); i++) { String[] commRow = commissionRateFile.getTupleByIndex(i); if (tier == Integer.valueOf(commRow[0]) && // match tier tradeQty >= Integer.valueOf(commRow[3]) && // match quantity tradeQty <= Integer.valueOf(commRow[4])) { String[] ttRow = tradeTypeFile.getTupleByIndex(tradeType); if (ttRow[0].equals(commRow[1]) && // match trade type secHandler.getSecRecord(newTrade.secFileIndex)[4].trim().equals(commRow[2])) { // match exchange (the file contains ws symbols?) //found the correct commission rate addTrade.commission = EGenMoney.mulMoneyByInt(newTrade.tradePrice, tradeQty); addTrade.commission.multiplyByDouble(Double.valueOf(commRow[5]) / 100.0); return; } } } //should never reach here assert false; } private void generateTradeTax() { if (addTrade.sellValue.lessThanOrEqual(addTrade.buyValue)) { // no capital gain addTrade.tax = new EGenMoney(0); return; } long addrId = addrGenerator.getAddrIdForCustomer(newTrade.customer); int[] countryAndDivisionCodes = addrGenerator.getCountryAndDivCodes(addrId); double countryRate = custTaxGenerator.getTaxRate(newTrade.customer, countryAndDivisionCodes[0], true); double divRate = custTaxGenerator.getTaxRate(newTrade.customer, countryAndDivisionCodes[1], false); /* * Comment from the original EGen: * * Do a trick for proper rounding of resulting tax amount. * Txn rates (fCtryRate and fDivRate) have 4 digits after a floating point * so the existing CMoney class is not suitable to round them (because CMoney * only keeps 2 digits after the point). Therefore need to do the manual trick * of multiplying tax rates by 10000.0 (not 100.0), adding 0.5, and truncating * to int to get the proper rounding. * * This is all to match the database calculation of T_TAX done by runtime transactions. */ addTrade.tax = EGenMoney.subMoney(addTrade.sellValue, addTrade.buyValue); // proceedings addTrade.tax.multiplyByDouble(((double)((int)(10000.0 * (countryRate + divRate) + 0.5)) / 10000.0)); } private void generateSettlementAmount() { String[] ttRow = tradeTypeFile.getTupleByIndex(newTrade.tradeType.ordinal()); if (Integer.valueOf(ttRow[2]) == 1) { // is sell? addTrade.settlement = EGenMoney.mulMoneyByInt(newTrade.tradePrice, newTrade.tradeQty); addTrade.settlement.sub(addTrade.charge); addTrade.settlement.sub(addTrade.commission); } else { addTrade.settlement = EGenMoney.mulMoneyByInt(newTrade.tradePrice, newTrade.tradeQty); addTrade.settlement.add(addTrade.charge); addTrade.settlement.add(addTrade.commission); addTrade.settlement.multiplyByInt(-1); } switch (addTrade.taxStatus) { case eNonTaxable: break; case eTaxableAndWithhold: addTrade.settlement.sub(addTrade.tax); break; case eTaxableAndDontWithhold: break; } } private void generateCompletedTradeInfo() { addTrade.taxStatus = custAccGenerator.getAccountTaxStatus(newTrade.accId); addTrade.brokerId = custAccGenerator.generateBrokerId(newTrade.accId); generateTradeCharge(); generateTradeCommission(); generateTradeTax(); generateSettlementAmount(); } private void generateNextTrade() { if (currCompletedTrades < totalTrades) { /* * While the earliest completion time is before the current * simulated ('Trade Order') time, keep creating new * incomplete trades putting them on the queue and * incrementing the current simulated time. */ while ((currCompletedTrades + currentTrades.size() < totalTrades) && (currentTrades.isEmpty() || currentSimulatedTime < currentTrades.peek().completionTime)) { currentSimulatedTime = (currInitiatedTrades / tradesPerWorkDay) * 3600 * 24 + // seconds per day (currInitiatedTrades % tradesPerWorkDay) * meanBetweenTrades + generateDelay(); generateNewTrade(); // ignore aborted trades if (HoldingsAndTrades.isAbortedTrade(currInitiatedTrades)) { continue; } currentTrades.add(newTrade); } newTrade = currentTrades.remove(); updateHoldings(); generateCompleteTrade(); hasNextTrade = currCompletedTrades < totalTrades; } else { hasNextTrade = false; } if (!hasNextTrade) { // find next holding for the holding generator holdingIterator = 0; findNextHolding(); // find a non-empty holding list for the holding summary findNextHoldingList(); assert currentTrades.size() == 0; } } private Object[] generateTradeRow() { Object[] tuple = new Object[getColumnNum(TPCEConstants.TABLENAME_TRADE)]; currTable = TPCEConstants.TABLENAME_TRADE; tuple[0] = newTrade.tradeId; // t_id tuple[1] = new TimestampType(getCurrentTradeCompletionTime()); // t_dts tuple[2] = statusTypeFile.getTupleByIndex(newTrade.tradeStatus.ordinal())[0]; // t_st_id tuple[3] = tradeTypeFile.getTupleByIndex(newTrade.tradeType.ordinal())[0]; // t_tt_id // is it a cash trade? addTrade.isCash = true; if ((newTrade.tradeType == TradeType.eMarketBuy || newTrade.tradeType == TradeType.eLimitBuy) && rnd.rndPercent(PERCENT_BUYS_ON_MARGIN)) { addTrade.isCash = false; } tuple[4] = addTrade.isCash ? 1 : 0; // t_is_cash tuple[5] = secHandler.createSymbol(newTrade.secFileIndex, 15); // t_s_symb; CHAR(15) tuple[6] = newTrade.tradeQty; // t_qty tuple[7] = newTrade.bidPrice.getDollars(); // t_bid_price tuple[8] = newTrade.accId; // t_ca_id tuple[9] = personHandler.getFirstName(newTrade.customer) + " " + personHandler.getLastName(newTrade.customer); // t_exec_name tuple[10] = newTrade.tradePrice.getDollars(); // t_trade_price tuple[11] = addTrade.charge.getDollars(); // t_chrg tuple[12] = addTrade.commission.getDollars(); // t_comm // t_tax switch (addTrade.taxStatus) { case eNonTaxable: tuple[13] = 0; break; case eTaxableAndWithhold: tuple[13] = addTrade.tax.getDollars(); break; case eTaxableAndDontWithhold: tuple[13] = addTrade.tax.getDollars(); break; } tuple[14] = newTrade.isLIFO ? 1 : 0; // t_lifo return tuple; } private Object[] generateSettlementRow() { Object[] tuple = new Object[getColumnNum(TPCEConstants.TABLENAME_SETTLEMENT)]; currTable = TPCEConstants.TABLENAME_SETTLEMENT; tuple[0] = newTrade.tradeId; // se_t_id tuple[1] = addTrade.isCash ? "Cash Account" : "Margin"; // se_cash_type tuple[2] = new TimestampType(EGenDate.addDaysMsecs(getCurrentTradeCompletionTime(), 2, 0, false)); // se_cash_due_date; 2 days after tuple[3] = addTrade.settlement.getDollars(); // se_amt return tuple; } private Object[] generateCashRow() { Object[] tuple = new Object[getColumnNum(TPCEConstants.TABLENAME_CASH_TRANSACTION)]; currTable = TPCEConstants.TABLENAME_CASH_TRANSACTION; tuple[0] = newTrade.tradeId; // ct_t_id tuple[1] = new TimestampType(getCurrentTradeCompletionTime()); // ct_dts tuple[2] = addTrade.settlement.getDollars(); // ct_amt tuple[3] = tradeTypeFile.getTupleByIndex(newTrade.tradeType.ordinal())[1] + " " + Integer.toString(newTrade.tradeQty) + " shares of " + secGenerator.createName(newTrade.secFileIndex); // ct_name return tuple; } private void generateTradeHistoryRows() { tradeHistory.clear(); currTable = TPCEConstants.TABLENAME_TRADE_HISTORY; int columnsNum = getColumnNum(TPCEConstants.TABLENAME_TRADE_HISTORY); if (newTrade.tradeType == TradeType.eStopLoss || newTrade.tradeType == TradeType.eLimitSell || newTrade.tradeType == TradeType.eLimitBuy) { for (int i = 0; i < 3; i++) { Object[] tuple = new Object[columnsNum]; tuple[0] = newTrade.tradeId; // th_t_id switch (i) { case 0: tuple[1] = new TimestampType(getCurrentTradePendingTime()); // th_dts tuple[2] = statusTypeFile.getTupleByIndex(StatusTypeId.E_PENDING.ordinal())[0]; // th_st_id break; case 1: tuple[1] = new TimestampType(getCurrentTradeSubmissionTime()); // th_dts tuple[2] = statusTypeFile.getTupleByIndex(StatusTypeId.E_SUBMITTED.ordinal())[0]; // th_st_id break; case 2: tuple[1] = new TimestampType(getCurrentTradeCompletionTime()); // th_dts tuple[2] = statusTypeFile.getTupleByIndex(StatusTypeId.E_COMPLETED.ordinal())[0]; // th_st_id break; default: assert false; } tradeHistory.add(tuple); } } else { for (int i = 0; i < 2; i++) { Object[] tuple = new Object[columnsNum]; tuple[0] = newTrade.tradeId; // th_t_id switch (i) { case 0: tuple[1] = new TimestampType(getCurrentTradeSubmissionTime()); // th_dts tuple[2] = statusTypeFile.getTupleByIndex(StatusTypeId.E_SUBMITTED.ordinal())[0]; // th_st_id break; case 1: tuple[1] = new TimestampType(getCurrentTradeCompletionTime()); // th_dts tuple[2] = statusTypeFile.getTupleByIndex(StatusTypeId.E_COMPLETED.ordinal())[0]; // th_st_id break; default: assert false; } tradeHistory.add(tuple); } } } private Object[] generateNextHoldingSummaryRow() { // we should have at least one tuple here assert currAccountForHoldingSummary < LOAD_UNIT_ACCOUNT_COUNT; Object[] tuple = new Object[getColumnNum(TPCEConstants.TABLENAME_HOLDING_SUMMARY)]; currTable = TPCEConstants.TABLENAME_HOLDING_SUMMARY; tuple[0] = currAccountForHoldingSummary + startFromAccount; // hs_ca_id // symbol long secFlatFileIndex = holdsGenerator.getSecurityFlatFileIndex(currAccountForHoldingSummary + startFromAccount, currSecurityForHoldingSummary + 1); tuple[1] = secHandler.createSymbol(secFlatFileIndex, 15); // hs_s_symb; CHAR(15) // total quantity over holdings int hsQty = 0; for (HoldingInfo hold: customerHoldings[currAccountForHoldingSummary][currSecurityForHoldingSummary]) { hsQty += hold.tradeQty; } tuple[2] = hsQty; // hs_qty hasNextHoldingSummary = findNextHoldingList(); return tuple; } private Object[] generateNextHoldingRow() { // we should have at least one tuple here assert currAccountForHolding < LOAD_UNIT_ACCOUNT_COUNT; Object[] tuple = new Object[getColumnNum(TPCEConstants.TABLENAME_HOLDING)]; currTable = TPCEConstants.TABLENAME_HOLDING; HoldingInfo holding = customerHoldings[currAccountForHolding][currSecurityForHolding].get(holdingIterator); tuple[0] = holding.tradeId; // h_t_id tuple[1] = currAccountForHolding + startFromAccount; // h_ca_id tuple[2] = secHandler.createSymbol(holding.symbolIndex, 15); // h_s_symb; CHAR(15) tuple[3] = new TimestampType(holding.buyDTS); // h_dts tuple[4] = holding.tradePrice.getDollars(); // h_price tuple[5] = holding.tradeQty; // h_qty holdingIterator++; hasNextHolding = findNextHolding(); return tuple; } /* (non-Javadoc) * @see java.util.Iterator#hasNext() */ public boolean hasNext() { if (currState == State.stNone) { currentLoadUnit++; if (currentLoadUnit == totalLoadUnits) { // all units are loaded return false; } initNextLoadUnit(); currState = State.stTrade; return true; } // we always have tuples in other states return true; } /* (non-Javadoc) * @see java.util.Iterator#next() */ public Object[] next() { Object[] tuple = null; switch (currState) { case stNone: assert false; tuple = null; break; case stTrade: generateNextTrade(); // generating all the necessary info about the trade tuple = generateTradeRow(); currState = State.stTradeHistory; break; case stTradeHistory: if (tradeHistoryCounter == 0) { generateTradeHistoryRows(); } tuple = tradeHistory.get(tradeHistoryCounter++); if (tradeHistoryCounter == tradeHistory.size()) { tradeHistoryCounter = 0; currState = State.stSettle; } break; case stSettle: tuple = generateSettlementRow(); currState = addTrade.isCash ? State.stCash : State.stHoldHistory; break; case stCash: tuple = generateCashRow(); currState = State.stHoldHistory; break; case stHoldHistory: // we always have at least one tuple here assert holdingHistory.size() != 0; currTable = TPCEConstants.TABLENAME_HOLDING_HISTORY; tuple = holdingHistory.get(holdHistoryCounter++); if (holdHistoryCounter == holdingHistory.size()) { holdHistoryCounter = 0; currState = hasNextTrade ? State.stTrade : State.stBroker; } break; case stBroker: // we always have at least one BROKER tuple assert brokerGenerator.hasNext(); currTable = TPCEConstants.TABLENAME_BROKER; tuple = brokerGenerator.next(); if (!brokerGenerator.hasNext()) { currState = State.stHoldSummary; } break; case stHoldSummary: // we always have at least one tuple assert hasNextHoldingSummary; tuple = generateNextHoldingSummaryRow(); if (!hasNextHoldingSummary) { currState = State.stHolding; } break; case stHolding: // we always have at least one tuple assert hasNextHolding; tuple = generateNextHoldingRow(); if (!hasNextHolding) { currState = State.stNone; // finished with the unit } break; } return tuple; } /** * Returns the table for which the previous next() call just * returned the tuple. * * @return Table name for the last generated tuple */ public String getCurrentTable() { return currTable; } public void remove() { throw new NotImplementedException("Remove not implemented"); } private int getColumnNum(String tableName) { return catalogDb.getTables().get(tableName).getColumns().size(); } private Date getCurrentTradeCompletionTime() { int daysFromStart = (int)(newTrade.completionTime / (3600 * 24)); int msecsFromStart = (int)((newTrade.completionTime - daysFromStart * 3600 * 24) * 1000); return EGenDate.addDaysMsecs(startTime, daysFromStart, msecsFromStart, true); // add days and msec and adjust for weekend } private Date getCurrentTradePendingTime() { int daysFromStart = (int)(newTrade.pendingTime / (3600 * 24)); int msecsFromStart = (int)((newTrade.pendingTime - daysFromStart * 3600 * 24) * 1000); return EGenDate.addDaysMsecs(startTime, daysFromStart, msecsFromStart, true); // add days and msec and adjust for weekend } private Date getCurrentTradeSubmissionTime() { int daysFromStart = (int)(newTrade.submissionTime / (3600 * 24)); int msecsFromStart = (int)((newTrade.submissionTime - daysFromStart * 3600 * 24) * 1000); return EGenDate.addDaysMsecs(startTime, daysFromStart, msecsFromStart, true); // add days and msec and adjust for weekend } }