package org.cryptocoinpartners.module.xchange; import java.lang.reflect.InvocationTargetException; import java.math.BigDecimal; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.Executors; import java.util.concurrent.RejectedExecutionException; import javax.annotation.Nullable; import javax.inject.Inject; import javax.inject.Singleton; import org.apache.commons.configuration.Configuration; import org.cryptocoinpartners.module.Context; import org.cryptocoinpartners.schema.Book; import org.cryptocoinpartners.schema.BookFactory; import org.cryptocoinpartners.schema.Exchange; import org.cryptocoinpartners.schema.Listing; import org.cryptocoinpartners.schema.Market; import org.cryptocoinpartners.schema.Prompt; import org.cryptocoinpartners.schema.TradeFactory; import org.cryptocoinpartners.util.CompareUtils; import org.cryptocoinpartners.util.EM; import org.cryptocoinpartners.util.RateLimiter; import org.cryptocoinpartners.util.XchangeUtil; import org.joda.time.Duration; import org.joda.time.Instant; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import com.xeiam.xchange.currency.CurrencyPair; import com.xeiam.xchange.dto.Order.OrderType; import com.xeiam.xchange.dto.marketdata.OrderBook; import com.xeiam.xchange.dto.marketdata.Trade; import com.xeiam.xchange.dto.marketdata.Trades; import com.xeiam.xchange.dto.trade.LimitOrder; import com.xeiam.xchange.okcoin.FuturesContract; import com.xeiam.xchange.service.streaming.ExchangeEvent; import com.xeiam.xchange.service.streaming.ExchangeEventType; import com.xeiam.xchange.service.streaming.ExchangeStreamingConfiguration; import com.xeiam.xchange.service.streaming.StreamingExchangeService; /** * @author Tim Olson */ @Singleton public class XchangeData { @Inject public XchangeData(Context context, Configuration config, BookFactory bookFactory, TradeFactory tradeFactory) { this.context = context; this.bookFactory = bookFactory; this.tradeFactory = tradeFactory; final String configPrefix = "xchange"; Set<String> exchangeTags = XchangeUtil.getExchangeTags(); // now we have all the exchange tags. process each config group for (String tag : exchangeTags) { // three configs required: // .class the full classname of the Xchange implementation // .rate.queries rate limit the number of queries to this many (default: 1) // .rate.period rate limit the number of queries during this period of time (default: 1 second) // .listings identifies which Listings should be fetched from this exchange Exchange exchange = XchangeUtil.getExchangeForTag(tag); String prefix = configPrefix + "." + tag + '.'; if (exchange != null) { final String helperClassName = config.getString(prefix + "helper.class", null); final String streamingConfigClassName = config.getString(prefix + "streaming.config.class", null); int queries = config.getInt(prefix + "rate.queries", 1); int retryCount = config.getInt(prefix + "retry", 10); Duration period = Duration.millis((long) (1000 * config.getDouble(prefix + "rate.period", 1))); // rate.period in seconds final List listings = config.getList(prefix + "listings"); initExchange(helperClassName, streamingConfigClassName, queries, period, exchange, listings, retryCount); } else { log.warn("Could not find Exchange for property \"xchange." + tag + ".*\""); } } } /** You may implement this interface to customize the interaction with the Xchange library for each exchange. Set the class name of your Helper in the module configuration using the key:<br/> xchange.<marketname>.helper.class=com.foo.bar.MyHelper<br/> if you leave out the package name it is assumed to be the same as the XchangeData class (i.e. the xchange module package). */ public interface Helper { Object[] getTradesParameters(CurrencyPair pair, long lastTradeTime, long lastTradeId); Object[] getOrderBookParameters(CurrencyPair pair); void handleTrades(Trades tradeSpec); void handleOrderBook(OrderBook orderBook); } private void initExchange(@Nullable String helperClassName, @Nullable String streamingConfigClassName, int queries, Duration per, Exchange coinTraderExchange, List listings, int retryCount) { com.xeiam.xchange.Exchange xchangeExchange = XchangeUtil.getExchangeForMarket(coinTraderExchange); StreamingExchangeService streamingDataService; Helper helper = null; if (helperClassName != null && !helperClassName.isEmpty()) { if (helperClassName.indexOf('.') == -1) helperClassName = XchangeData.class.getPackage().getName() + '.' + helperClassName; try { final Class<?> helperClass = getClass().getClassLoader().loadClass(helperClassName); try { helper = (Helper) helperClass.newInstance(); } catch (InstantiationException | IllegalAccessException e) { log.error("Could not initialize XchangeData because helper class " + helperClassName + " could not be instantiated ", e); return; } catch (ClassCastException e) { log.error("Could not initialize XchangeData because helper class " + helperClassName + " does not implement " + Helper.class); return; } } catch (ClassNotFoundException e) { log.error("Could not initialize XchangeData because helper class " + helperClassName + " was not found"); return; } } ExchangeStreamingConfiguration streamingConfiguration = null; if (streamingConfigClassName != null && !streamingConfigClassName.isEmpty()) { } List<Market> markets = new ArrayList<>(listings.size()); Market market; // ExchangeStreamingConfiguration streamingConfiguration = new OkCoinExchangeStreamingConfiguration(); for (Iterator<List> il = listings.iterator(); il.hasNext();) { Object listingSymbol = il.next(); Listing listing = Listing.forSymbol(listingSymbol.toString().toUpperCase()); market = context.getInjector().getInstance(Market.class).findOrCreate(coinTraderExchange, listing); markets.add(market); } if (streamingConfigClassName != null) { RateLimiter rateLimiter = new RateLimiter(queries, per); // streamingDataService = xchangeExchange.getStreamingExchangeService(streamingConfiguration); for (Iterator<Market> im = markets.iterator(); im.hasNext();) { market = im.next(); ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()); ListenableFuture<StreamTradesRunnable> streamingTradesFuture = service.submit(new StreamTradesRunnable(context, xchangeExchange, market, rateLimiter, streamingConfigClassName, helper)); Futures.addCallback(streamingTradesFuture, new FutureCallback<StreamTradesRunnable>() { // we want this handler to run immediately after we push the big red button! @Override public void onSuccess(StreamTradesRunnable streamingTradesFuture) { System.out.println("complete"); //walkAwayFrom(explosion); } @Override public void onFailure(Throwable thrown) { System.out.println("failed"); //battleArchNemesis(); // escaped the explosion! } }); } return; } else { RateLimiter rateLimiter = new RateLimiter(queries, per); // initExchange(helperClassName, streamingConfigClassName, queries, period, exchange, listings); // (@Nullable String helperClassName, @Nullable String streamingConfigClassName, int queries, Duration per, // Exchange coinTraderExchange, List listings for (Iterator<Market> im = markets.iterator(); im.hasNext(); rateLimiter.execute(new FetchTradesRunnable(context, helperClassName, streamingConfigClassName, queries, per, coinTraderExchange, retryCount, xchangeExchange, market, rateLimiter, helper))) market = im.next(); return; } } private class StreamTradesRunnable implements Callable { private final Helper helper; DateFormat dateFormat = new SimpleDateFormat("ddMMyy"); private ExchangeStreamingConfiguration streamingConfiguration; private CurrencyPair[] pairs; public StreamTradesRunnable(Context context, com.xeiam.xchange.Exchange xchangeExchange, Market market, RateLimiter rateLimiter, String streamingConfigClassName, @Nullable Helper helper) { this.context = context; this.rateLimiter = rateLimiter; this.market = market; this.helper = helper; this.prompt = market.getListing().getPrompt(); pairs = new CurrencyPair[] { XchangeUtil.getCurrencyPairForListing(market.getListing()) }; contract = prompt == null ? null : XchangeUtil.getContractForListing(market.getListing()); // Thread.currentThread().setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { // @Override // public void uncaughtException(Thread t, Throwable e) { // e.printStackTrace(); // } // }); if (streamingConfigClassName.indexOf('.') == -1) streamingConfigClassName = XchangeData.class.getPackage().getName() + '.' + streamingConfigClassName; try { final Class<?> streamingConfigClass = getClass().getClassLoader().loadClass(streamingConfigClassName); // CurrencyPair[] ccy = new CurrencyPair[] { CurrencyPair.BTC_USD }; try { streamingConfiguration = (ExchangeStreamingConfiguration) CompareUtils.tryToCreateBestMatch(streamingConfigClass, new Object[] { pairs }); dataService = xchangeExchange.getStreamingExchangeService(streamingConfiguration); dataService.connect(); String str = streamingConfigClass.getCanonicalName(); } catch (InstantiationException e1) { // TODO Auto-generated catch block log.error("Threw a Execption, full stack trace follows:", e1); e1.printStackTrace(); } catch (IllegalAccessException e1) { // TODO Auto-generated catch block log.error("Threw a Execption, full stack trace follows:", e1); e1.printStackTrace(); } catch (InvocationTargetException e1) { // TODO Auto-generated catch block log.error("Threw a Execption, full stack trace follows:", e1); e1.printStackTrace(); } catch (Exception | Error e2) { // TODO Auto-generated catch block log.error("Threw a Execption, full stack trace follows:", e2); e2.printStackTrace(); } try { streamingConfiguration = (ExchangeStreamingConfiguration) streamingConfigClass.newInstance(); // StreamingExchangeService service = xchangeExchange.getStreamingExchangeService(new (ExchangeStreamingConfiguration) streamingConfigClass(new CurrencyPair[]{ CurrencyPair.BTC_USD })); } catch (InstantiationException | IllegalAccessException e) { log.error("Could not initialize XchangeData because stremaing configuration class " + streamingConfigClassName + " could not be instantiated ", e); return; } catch (ClassCastException e) { log.error("Could not initialize XchangeData because stremaing configuration class " + streamingConfigClassName + " does not implement " + ExchangeStreamingConfiguration.class); return; } catch (Exception | Error e2) { // TODO Auto-generated catch block log.error("Threw a Execption, full stack trace follows:", e2); e2.printStackTrace(); return; } } catch (ClassNotFoundException e) { log.error("Could not initialize XchangeData because stremaing configuration class " + streamingConfigClassName + " was not found"); return; } catch (Exception | Error e2) { log.error("Threw a Execption, full stack trace follows:", e2); // TODO Auto-generated catch block e2.printStackTrace(); } Class<? extends ExchangeStreamingConfiguration> myclass = streamingConfiguration.getClass(); Class<?> streamingConfigClass = null; try { streamingConfigClass = getClass().getClassLoader().loadClass(streamingConfigClassName); } catch (ClassNotFoundException e) { // TODO Auto-generated catch block log.error("Threw a Execption, full stack trace follows:", e); e.printStackTrace(); } catch (Exception | Error e2) { // TODO Auto-generated catch block log.error("Threw a Execption, full stack trace follows:", e2); e2.printStackTrace(); } try { streamingConfiguration = (ExchangeStreamingConfiguration) streamingConfigClass.newInstance(); } catch (InstantiationException e) { // TODO Auto-generated catch block log.error("Threw a Execption, full stack trace follows:", e); e.printStackTrace(); } catch (IllegalAccessException e) { // TODO Auto-generated catch block log.error("Threw a Execption, full stack trace follows:", e); e.printStackTrace(); } catch (Exception | Error e2) { log.error("Threw a Execption, full stack trace follows:", e2); // TODO Auto-generated catch block e2.printStackTrace(); } // EntityManager entityManager = //.createEntityManager(); try { List<org.cryptocoinpartners.schema.Trade> results = EM.queryList(org.cryptocoinpartners.schema.Trade.class, "select t from Trade t where market=?1 and time=(select max(time) from Trade where market=?1)", market); for (org.cryptocoinpartners.schema.Trade trade : results) { // org.cryptocoinpartners.schema.Trade trade = query.getSingleResult(); long millis = trade.getTime().getMillis(); if (millis > lastTradeTime) lastTradeTime = millis; // todo this is broken and assumes an increasing integer remote key // Long remoteId = Long.valueOf(trade.getRemoteKey().concat(String.valueOf(trade.getTimestamp()))); Long remoteId = Long.valueOf(trade.getRemoteKey()); if (remoteId > lastTradeId) lastTradeId = remoteId; } } finally { // EM.em().close(); } //StreamingExchangeService streamingDataService = xchangeExchange.getStreamingExchangeService(streamingConfiguration,new CurrencyPair[]{ CurrencyPair.BTC_USD })); lastTradeTime = 0; lastTradeId = 0; } @Override public Object call() { try { while (true) { ExchangeEvent event = dataService.getNextEvent(); if (event != null) { //System.out.println("---> " + event.getPayload() + " " + event.getEventType()); if (event.getEventType().equals(ExchangeEventType.TRADE)) { com.xeiam.xchange.dto.marketdata.Trade trade = (com.xeiam.xchange.dto.marketdata.Trade) event.getPayload(); long remoteId = Long.valueOf(String.valueOf(dateFormat.format(trade.getTimestamp()).concat(trade.getId()))).longValue(); if (remoteId > lastTradeId) { Instant tradeInstant = new Instant(trade.getTimestamp()); BigDecimal volume = (trade.getType() == OrderType.ASK) ? trade.getTradableAmount().negate() : trade.getTradableAmount(); org.cryptocoinpartners.schema.Trade ourTrade = tradeFactory.create(market, tradeInstant, trade.getId(), trade.getPrice(), volume); context.publish(ourTrade); if (ourTrade.getDao() == null) System.out.println("empty thingh"); lastTradeTime = tradeInstant.getMillis(); lastTradeId = remoteId; } } else if (event.getEventType().equals(ExchangeEventType.DEPTH)) { OrderBook orderBook = (OrderBook) event.getPayload(); if (helper != null) helper.handleOrderBook(orderBook); Book book = bookFactory.create(new Instant(orderBook.getTimeStamp()), market); // bookBuilder.start(new Instant(orderBook.getTimeStamp()), null, market); LimitOrder limitOrder; for (Iterator<LimitOrder> itb = orderBook.getBids().iterator(); itb.hasNext(); book.addBid(limitOrder.getLimitPrice(), limitOrder.getTradableAmount())) limitOrder = itb.next(); for (Iterator<LimitOrder> ita = orderBook.getAsks().iterator(); ita.hasNext(); book.addAsk(limitOrder.getLimitPrice(), limitOrder.getTradableAmount())) limitOrder = ita.next(); book.build(); context.publish(book); } if (event.getEventType().equals(ExchangeEventType.DISCONNECT)) { log.error(this.getClass().getSimpleName() + " Disconnected"); //Thread.currentThread().interrupt(); //dataService. // dataService.disconnect(); // dataService.connect(); //READYSTATE status = dataService.getWebSocketStatus(); // dataService.connect(); // let's resubmit and connect } } } } catch (InterruptedException e) { log.error("Threw a Execption, full stack trace follows disconnecting:", e); // Thread.currentThread().interrupt(); dataService.disconnect(); // Thread.currentThread().interrupt(); } catch (RejectedExecutionException rej) { log.error("Threw a Execption, full stack trace follows:", rej); rej.printStackTrace(); } catch (Exception | Error e2) { // TODO Auto-generated catch block log.error("Threw a Execption, full stack trace follows:", e2); e2.printStackTrace(); } return Thread.currentThread(); } // private final Book.Builder bookBuilder = new Book.Builder(); private final boolean getTradesNext = true; private StreamingExchangeService dataService = null; private final Context context; private final Market market; private final RateLimiter rateLimiter; private final FuturesContract contract; private long lastTradeTime; private final Prompt prompt; private long lastTradeId; } private class FetchTradesRunnable implements Runnable { private final Helper helper; DateFormat dateFormat = new SimpleDateFormat("ddMMyy"); // @Nullable String helperClassName, @Nullable String streamingConfigClassName, int queries, Duration per, // Exchange coinTraderExchange, List listings public FetchTradesRunnable(Context context, @Nullable String helperClassName, @Nullable String streamingConfigClassName, int queries, Duration per, Exchange coinTraderExchange, int restartCount, com.xeiam.xchange.Exchange xchangeExchange, Market market, RateLimiter rateLimiter, @Nullable Helper helper) { this.helperClassName = helperClassName; this.streamingConfigClassName = streamingConfigClassName; this.queries = queries; this.period = per; this.coinTraderExchange = coinTraderExchange; this.restartCount = restartCount; this.context = context; this.market = market; this.rateLimiter = rateLimiter; this.xchangeExchange = xchangeExchange; this.helper = helper; this.prompt = market.getListing().getPrompt(); pair = XchangeUtil.getCurrencyPairForListing(market.getListing()); contract = prompt == null ? null : XchangeUtil.getContractForListing(market.getListing()); lastTradeTime = 0; lastTradeId = 0; // EntityManager entityManager = PersistUtil.createEntityManager(); try { List<org.cryptocoinpartners.schema.Trade> results = EM.queryList(org.cryptocoinpartners.schema.Trade.class, "select t from Trade t where market=?1 and time=(select max(time) from Trade where market=?1)", market); for (org.cryptocoinpartners.schema.Trade trade : results) { // org.cryptocoinpartners.schema.Trade trade = query.getSingleResult(); long millis = trade.getTime().getMillis(); if (millis > lastTradeTime) lastTradeTime = millis; // todo this is broken and assumes an increasing integer remote key // Long remoteId = Long.valueOf(trade.getRemoteKey().concat(String.valueOf(trade.getTimestamp()))); Long remoteId = Long.valueOf(trade.getRemoteKey()); if (remoteId > lastTradeId) lastTradeId = remoteId; } } catch (Exception | Error e) { log.error(this.getClass().getSimpleName() + ":FetchTradesRunnable Unabel to query last trade id"); } } @Override public void run() { try { getTrades(); getBook(); } catch (Exception | Error e) { log.error(this.getClass().getSimpleName() + ":run. Unable to retrive book or trades for market:" + market); //Thread.currentThread(). // throw e; //Thread.currentThread().interrupt(); // throw e; } finally { // getTradesNext = !getTradesNext; rateLimiter.execute(this); // run again. requeue } } protected void getTrades() throws Exception { try { Object params[]; if (helper != null) params = helper.getTradesParameters(pair, lastTradeTime, lastTradeId); else { if (contract == null) params = new Object[] {}; else params = new Object[] { contract }; } log.trace("Attempting to get trades from data service"); Trades tradeSpec = XchangeUtil.getExchangeForMarket(coinTraderExchange).getPollingMarketDataService().getTrades(pair, params); if (helper != null) helper.handleTrades(tradeSpec); List<com.xeiam.xchange.dto.marketdata.Trade> trades = tradeSpec.getTrades(); log.trace("sorting trades by time and id: " + trades); Collections.sort(trades, timeOrderIdComparator); // Iterator<Trade> ilt = trades.iterator(); // log.trace("itterating over sorted trades: " + trades.size()); for (Trade trade : trades) { // do { // if (!ilt.hasNext()) // break; // com.xeiam.xchange.dto.marketdata.Trade trade = ilt.next(); long remoteId = Long.valueOf(String.valueOf(dateFormat.format(trade.getTimestamp()).concat(trade.getId()))).longValue(); if (remoteId > lastTradeId) { Instant tradeInstant = new Instant(trade.getTimestamp()); BigDecimal volume = (trade.getType() == OrderType.ASK) ? trade.getTradableAmount().negate() : trade.getTradableAmount(); log.trace("Creating new cointrader trades from: " + trade); org.cryptocoinpartners.schema.Trade ourTrade = tradeFactory.create(market, tradeInstant, trade.getId(), trade.getPrice(), volume); context.publish(ourTrade); if (ourTrade.getDao() == null) System.out.println("duffer"); lastTradeTime = tradeInstant.getMillis(); lastTradeId = remoteId; } } tradeFailureCount = 0; return; } catch (Exception | Error e) { tradeFailureCount++; log.error(this.getClass().getSimpleName() + ":getTrades Unabel to get trade for market " + market + " pair " + pair + ". Failure " + tradeFailureCount + " of " + restartCount + ". Full Stack Trace: " + e); if (tradeFailureCount >= restartCount) { //try { // if (rateLimiter.getRunnables() == null || rateLimiter.getRunnables().isEmpty() || rateLimiter.remove(this)) { log.error(this.getClass().getSimpleName() + ":getTrades Unabel to get trade for market " + market + " pair " + pair + " for " + tradeFailureCount + " of " + restartCount + " time. Resetting Data Service Connection."); com.xeiam.xchange.Exchange xchangeExchange = XchangeUtil.resetExchange(coinTraderExchange); // dataService = xchangeExchange.getPollingMarketDataService(); tradeFailureCount = 0; bookFailureCount = 0; throw e; //} // } catch (Throwable e) { // TODO Auto-generated catch block // throw e; // } //Thread.currentThread(). // dataService = xchangeExchange.getPollingMarketDataService(); } } } protected void getBook() throws Exception { try { Object params[]; if (helper != null) params = helper.getOrderBookParameters(pair); else { if (contract == null) params = new Object[] {}; else params = new Object[] { contract }; } log.trace("Attempting to get book from data service"); OrderBook orderBook = XchangeUtil.getExchangeForMarket(coinTraderExchange).getPollingMarketDataService().getOrderBook(pair, params); if (helper != null) helper.handleOrderBook(orderBook); log.trace("Attempting create book from: " + orderBook); Book book = bookFactory.create(new Instant(orderBook.getTimeStamp()), market); LimitOrder limitOrder; // sort lowerst to highest limit price List<LimitOrder> asks = orderBook.getAsks(); log.trace("Attempting to sort asks: " + asks); Collections.sort(asks, limitPriceComparator); // sort highest to lowest limit price List<LimitOrder> bids = orderBook.getBids(); log.trace("Attempting to sort bids: " + bids); Collections.sort(bids, Collections.reverseOrder(limitPriceComparator)); // need to sort bids for (Iterator<LimitOrder> itb = bids.iterator(); itb.hasNext(); book.addBid(limitOrder.getLimitPrice(), limitOrder.getTradableAmount())) limitOrder = itb.next(); // neet to sort asks for (Iterator<LimitOrder> ita = asks.iterator(); ita.hasNext(); book.addAsk(limitOrder.getLimitPrice(), limitOrder.getTradableAmount())) limitOrder = ita.next(); book.build(); // log.debug("publish book:" + book.getId()); context.publish(book); bookFailureCount = 0; } catch (Exception | Error e) { bookFailureCount++; log.error(this.getClass().getSimpleName() + ":getTrades Unabel to get trade for market " + market + " pair " + pair + ". Failure " + bookFailureCount + " of " + restartCount + ". Full Stack Trace: " + e); if ((bookFailureCount >= restartCount)) { log.error(this.getClass().getSimpleName() + ":getBook Unabel to get book for market " + market + " pair " + pair + ". Failure " + bookFailureCount + " of " + restartCount + " . Resetting Data Service Connection."); com.xeiam.xchange.Exchange xchangeExchange = XchangeUtil.resetExchange(coinTraderExchange); tradeFailureCount = 0; bookFailureCount = 0; // failureCount=0; throw e; //} // } catch (Throwable e) { // TODO Auto-generated catch block // e.printStackTrace(); // } } } } // private final Book.Builder bookBuilder = new Book.Builder(); private final boolean getTradesNext = true; private final boolean restarted = false; private final String helperClassName; private final String streamingConfigClassName; private final int queries; private final Duration period; private final Exchange coinTraderExchange; private final int restartCount; private int tradeFailureCount = 0; private int bookFailureCount = 0; private final RateLimiter rateLimiter; private final Context context; private final Market market; private final CurrencyPair pair; private final FuturesContract contract; private long lastTradeTime; private final Prompt prompt; private long lastTradeId; private final com.xeiam.xchange.Exchange xchangeExchange; } private static final Comparator<LimitOrder> limitPriceComparator = new Comparator<LimitOrder>() { @Override public int compare(LimitOrder event, LimitOrder event2) { return event.getLimitPrice().compareTo(event2.getLimitPrice()); } }; private static final Comparator<Trade> timeOrderIdComparator = new Comparator<Trade>() { @Override public int compare(Trade event, Trade event2) { int sComp = event.getTimestamp().compareTo(event2.getTimestamp()); if (sComp != 0) { return sComp; } else { return (event.getId().compareTo(event2.getId())); } } }; protected static Logger log = LoggerFactory.getLogger("org.cryptocoinpartners.xchangeData"); private final BookFactory bookFactory; private final boolean orderByTime = true; private final TradeFactory tradeFactory; // @Inject //protected EntityManager entityManager; private final Context context; }