package jtrade.marketfeed;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NavigableMap;
import java.util.Timer;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicInteger;

import jtrade.JTradeException;
import jtrade.Symbol;
import jtrade.Symbol.SymbolType;
import jtrade.SymbolFactory;
import jtrade.util.Configurable;
import jtrade.util.Util;

import org.joda.time.DateTime;
import org.joda.time.Days;
import org.joda.time.Duration;
import org.joda.time.Interval;
import org.joda.time.format.DateTimeFormat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.ib.client.CommissionReport;
import com.ib.client.Contract;
import com.ib.client.ContractDetails;
import com.ib.client.EClientSocket;
import com.ib.client.EWrapper;
import com.ib.client.Execution;
import com.ib.client.Order;
import com.ib.client.OrderState;
import com.ib.client.ScannerSubscription;
import com.ib.client.TickType;
import com.ib.client.UnderComp;

/**
 * @author jonkle
 * 
 */
public class IBMarketFeed extends AbstractMarketFeed implements EWrapper {
	private static final Logger logger = LoggerFactory.getLogger(IBMarketFeed.class);
	private static final int historicalRequestTimeOutMillis = 30000;
	private static final int maxBarsPerHistoricalRequest = 2000;
	private static final double maxHistoricalRequestsPerSecond = 0.067;
	private static final int maxHistoricalRequestRetries = 2;
	private static final Duration realTimeBarSize = new Duration(5000);
	private static final int defaultPort = 4000;

	public final Configurable<String> SERVER_HOSTS = new Configurable<String>("SERVER_HOSTS", "localhost:4000");
	public final Configurable<Integer> CLIENT_ID = new Configurable<Integer>("CLIENT_ID", 1);

	protected String[] hosts;
	protected int[] ports;
	protected int currHost;
	protected EClientSocket socket;
	protected int serverVersion;
	protected boolean connect;
	protected int clientId;
	protected String accountCode;
	protected AtomicInteger nextValidReqId;
	protected String lastMessage;
	protected Map<Integer, IBTickRequest> tickRequestsById;
	protected Map<Integer, IBBarRequest> barRequestsById;
	protected Map<Integer, IBHistoricalRequest> historicalRequestsById;
	protected Map<Integer, IBScannerRequest> scannerRequestsById;
	protected Map<Integer, IBContractRequest> contractRequestsById;
	protected String marketScannerParams;
	protected Timer timer;
	protected long lastHistoricalRequestMillis;
	protected List<BarListener> barListeners;
	protected List<TickListener> tickListeners;

	public IBMarketFeed() {
		this(null, -1, (File) null);
	}

	public IBMarketFeed(int clientId) {
		this(null, clientId, (File) null);
	}

	public IBMarketFeed(String hosts, int clientId) {
		this(hosts, clientId, (File) null);
	}

	public IBMarketFeed(String hosts, int clientId, String dataDir) {
		this(hosts, clientId, new File(dataDir));
	}

	public IBMarketFeed(String hosts, int clientId, File dataDir) {
		super(dataDir);
		if (hosts == null) {
			hosts = SERVER_HOSTS.get();
		}
		if (clientId < 0) {
			clientId = CLIENT_ID.get();
		}
		String[] tmp = Util.split(hosts, ',');
		this.hosts = new String[tmp.length];
		this.ports = new int[tmp.length];
		for (int i = 0; i < tmp.length; i++) {
			String[] hp = Util.split(tmp[i].trim().toLowerCase(), ':');
			if (hp.length == 1) {
				this.hosts[i] = hp[0].trim();
				this.ports[i] = defaultPort;
			} else {
				this.hosts[i] = hp[0].trim();
				this.ports[i] = Integer.parseInt(hp[1]);
			}
		}
		this.clientId = clientId;
		nextValidReqId = new AtomicInteger();
		tickRequestsById = new HashMap<Integer, IBTickRequest>();
		barRequestsById = new HashMap<Integer, IBBarRequest>();
		historicalRequestsById = new HashMap<Integer, IBHistoricalRequest>();
		scannerRequestsById = new HashMap<Integer, IBScannerRequest>();
		contractRequestsById = new HashMap<Integer, IBContractRequest>();
		barListeners = new ArrayList<BarListener>();
		tickListeners = new ArrayList<TickListener>();
	}

	@Override
	protected void finalize() throws Throwable {
		disconnect();
	}

	@Override
	public synchronized void connect() {
		if (isConnected()) {
			return;
		}
		connect = true;
		try {
			doConnect();
		} catch (JTradeException e) {
			logger.warn(e.getMessage());
		} catch (Throwable t) {
			logger.error(t.getMessage(), t);
		}

		timer = new Timer(true);
		timer.scheduleAtFixedRate(new java.util.TimerTask() {
			@Override
			public void run() {
				try {
					if (connect && (socket == null || !socket.isConnected())) {
						doConnect();
					}
				} catch (JTradeException e) {
					logger.warn(e.getMessage());
				} catch (Throwable t) {
					logger.error(t.getMessage(), t);
				}
			}
		}, 15000, 15000);
		timer.scheduleAtFixedRate(new java.util.TimerTask() {
			@Override
			public void run() {
				DateTime now = new DateTime();
				if (now.getSecondOfMinute() == 0) {
					fireMinuteEvent(now);
					if (now.getMinuteOfHour() == 0) {
						fireHourEvent(now);
						if (now.getHourOfDay() == 0) {
							fireDayEvent(now);
						}
					}
					// Sometimes ticks stop being received, this might
					// workaround that.
					refreshTickRequests();
				}
				if (tickRequestsById.size() > 0) {
					for (IBTickRequest req : tickRequestsById.values()) {
						if (req.barListeners.size() > 0) {
							buildBar(req, -1, -1, -1, null);
						}
					}
				}
			}
		}, new DateTime().millisOfSecond().roundCeilingCopy().toDate(), 1000);
	}

	protected synchronized void doConnect() {
		socket = new EClientSocket(this);
		socket.eConnect(hosts[currHost], ports[currHost], clientId);
		if (!socket.isConnected()) {
			int failedHost = currHost;
			currHost = (currHost + 1) % hosts.length;
			throw new JTradeException(String.format("Could not connect to TWS using %s@%s:%s", clientId, hosts[failedHost], ports[failedHost]));
		}
		// IB Log levels: 1=SYSTEM 2=ERROR 3=WARNING 4=INFORMATION 5=DETAIL
		socket.setServerLogLevel(3);
		socket.reqNewsBulletins(true);
		serverVersion = socket.serverVersion();

		logger.info("Connected to TWS v{} using {}@{}:{}, data folder is {}",
				new Object[] { serverVersion, clientId, hosts[currHost], ports[currHost], dataDir.getAbsolutePath() });
	}

	@Override
	public synchronized void disconnect() {
		connect = false;
		if (timer != null) {
			timer.cancel();
			timer = null;
		}
		if (socket == null || !socket.isConnected()) {
			return;
		}

		removeAllListeners();

		if (socket != null && socket.isConnected()) {
			socket.cancelNewsBulletins();
			socket.eDisconnect();
		}
		socket = null;
		logger.info("Disconnected from TWS v{} using {}@{}:{}", new Object[] { serverVersion, clientId, hosts[currHost], hosts[currHost] });
	}

	@Override
	public boolean isConnected() {
		return socket != null && socket.isConnected();
	}

	protected void checkConnected() {
		if (!isConnected()) {
			throw new JTradeException(String.format("%s is not connected to TWS at %s@%s", getClass().getSimpleName(), clientId, SERVER_HOSTS.get()));
		}
	}

	private synchronized void refreshTickRequests() {
		long now = System.currentTimeMillis();
		for (IBTickRequest req : new ArrayList<IBTickRequest>(tickRequestsById.values())) {
			if (req.timestamp + 30000 > now) {
				continue;
			}
			logger.debug("Refreshing, removing {}", req);
			tickRequestsById.remove(req.id);
			if (req.marketDepth) {
				socket.cancelMktDepth(req.id);
			}
			socket.cancelMktData(req.id);

			Contract contract = makeContract(req.symbol);
			req.id = nextValidReqId.incrementAndGet();
			logger.debug("Refreshing, adding {}", req);
			socket.reqMktData(req.id, contract, "", false);
			if (req.marketDepth) {
				socket.reqMktDepth(req.id, contract, 10);
			}
			tickRequestsById.put(req.id, req);
		}
	}

	protected Contract makeContract(Symbol symbol) {
		String code = symbol.getCode();
		String exchange = symbol.getExchange();
		String currency = symbol.getCurrency();
		String securityType = null;
		String expiry = null;
		String multiplier = String.valueOf(symbol.getMultiplier());
		double strike = 0.0;
		String right = null;
		int conId = 0;
		boolean includeExpired = false;
		if (symbol.isStock()) {
			securityType = "STK";
		} else if (symbol.isFuture()) {
			securityType = "FUT";
			expiry = symbol.getExpiry() != null ? symbol.getExpiry().toString("yyyyMMdd") : null;
			includeExpired = true;
		} else if (symbol.isIndex()) {
			securityType = "IND";
		} else if (symbol.isOption()) {
			securityType = "OPT";
			expiry = symbol.getExpiry() != null ? symbol.getExpiry().toString("yyyyMMdd") : null;
			strike = symbol.getStrike();
			right = symbol.getOptionRight().toString();
			includeExpired = true;
		} else if (symbol.isCash()) {
			securityType = "CASH";
		}
		return makeContract(code, exchange, currency, securityType, expiry, multiplier, strike, right, conId, includeExpired);
	}

	protected Contract makeContract(String symbol, String exchange, String currency, String securityType, String expiry, String multiplier, double strike,
			String right, int conId, boolean includeExpired) {
		Contract contract = new Contract();
		contract.m_symbol = symbol;
		contract.m_exchange = exchange;
		contract.m_currency = currency;
		contract.m_secType = securityType;
		contract.m_expiry = expiry;
		contract.m_multiplier = multiplier;
		contract.m_strike = strike;
		contract.m_right = right;
		contract.m_conId = conId;
		contract.m_includeExpired = includeExpired;
		return contract;
	}

	protected Symbol toSymbol(Contract contract) {
		return toSymbol(contract, null);
	}

	protected Symbol toSymbol(Contract contract, ContractDetails details) {
		StringBuilder fullCode = new StringBuilder(contract.m_symbol).append('-')
				.append(contract.m_exchange != null ? contract.m_exchange : contract.m_primaryExch).append('-').append(contract.m_currency).append('-');
		if ("FUT".equals(contract.m_secType)) {
			fullCode.append("FUTURE");
		} else if ("STK".equals(contract.m_secType)) {
			fullCode.append("STOCK");
		} else if ("IND".equals(contract.m_secType)) {
			fullCode.append("INDEX");
		} else if ("OPT".equals(contract.m_secType)) {
			fullCode.append("OPTION");
		} else {
			fullCode.append("CASH");
		}
		if (contract.m_expiry != null) {
			fullCode.append('-').append(contract.m_expiry);
		}
		if (contract.m_strike > 0) {
			if (contract.m_strike - (long) contract.m_strike != 0.0) {
				fullCode.append('-').append(String.format("%.2f", contract.m_strike)).append('-').append(contract.m_right.toUpperCase());
			} else {
				fullCode.append('-').append((long) contract.m_strike).append('-').append(contract.m_right.toUpperCase());
			}
		}
		if (contract.m_right != null) {
			if (contract.m_right.equals("C") || contract.m_right.equals("CALL")) {
				fullCode.append('-').append("CALL");
			} else if (contract.m_right.equals("P") || contract.m_right.equals("PUT")) {
				fullCode.append('-').append("PUT");
			}
		}
		Symbol s = SymbolFactory.getSymbol(fullCode.toString());
		// s.multiplier = contract.m_multiplier != null ?
		// Integer.parseInt(contract.m_multiplier) : s.multiplier;
		// s.minTick = details != null ? details.m_minTick : s.minTick;
		return s;
	}

	@Override
	protected NavigableMap<DateTime, Bar> fetchHistoricalData(Symbol symbol, DateTime fromDate, DateTime toDate, int barSizeSeconds) {
		NavigableMap<DateTime, Bar> result = new TreeMap<DateTime, Bar>();

		DateTime start = toDate.isAfterNow() ? new DateTime(toDate.getZone()) : toDate;
		if (symbol.getExpiry() != null && start.isAfter(symbol.getExpiry())) {
			start = symbol.getExpiry();
		}
		DateTime end = null;

		logger.info("Fetching historical data for symbol {} {}-{}", new Object[] { symbol, fromDate, start });

		int retries = 0;
		while (true) {
			end = start;
			if (barSizeSeconds < 86400) {
				start = end.minusSeconds(Math.min(maxBarsPerHistoricalRequest * barSizeSeconds, 86400));
			} else {
				start = end.minusSeconds(maxBarsPerHistoricalRequest * barSizeSeconds);
			}
			if (start.isBefore(fromDate)) {
				start = fromDate;
			}

			while (1000.0 / (System.currentTimeMillis() - lastHistoricalRequestMillis) >= maxHistoricalRequestsPerSecond) {
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
				}
			}

			lastHistoricalRequestMillis = System.currentTimeMillis();

			IBHistoricalRequest req = requestHistoricalData(symbol, start, end, barSizeSeconds);
			if (req.failed) {
				throw new JTradeException(String.format("Historical data for symbol %s could not be fetched: %s", symbol, req.error));
			}
			for (Bar bar : req.bars.values()) {
				if ((bar.getDateTime().isEqual(fromDate) || bar.getDateTime().isAfter(fromDate)) && bar.getDateTime().isBefore(toDate)) {
					result.put(bar.getDateTime(), bar);
				}
			}
			if (req.bars.isEmpty()) {
				retries++;
				start = end;
			} else {
				start = req.bars.firstKey();
			}

			if (start.isBefore(fromDate) || start.isEqual(fromDate) || retries > maxHistoricalRequestRetries) {
				break;
			}
		}
		return result;
	}

	public IBHistoricalRequest requestHistoricalData(Symbol symbol, DateTime fromDate, DateTime toDate, int barSizeSeconds) {
		checkConnected();
		if (historicalRequestsById.size() > 0) {
			throw new IllegalStateException(String.format("%s historical request already in progress.", getClass().getSimpleName()));
		}
		Interval interval = new Interval(fromDate, toDate);
		if ((interval.toDurationMillis() / 1000) / barSizeSeconds > maxBarsPerHistoricalRequest) {
			throw new IllegalStateException(String.format("%s cannot request %s > %s ticks historical data at once.", getClass().getSimpleName(),
					(interval.toDurationMillis() / 1000) / barSizeSeconds, maxBarsPerHistoricalRequest));
		}

		String endDateStr = toDate.toString("yyyyMMdd HH:mm:ss");
		String durationStr = null;
		String barSizeSetting = null;
		switch (barSizeSeconds) {
		case 1:
			durationStr = (interval.toDurationMillis() / 1000) + " S";
			barSizeSetting = "1 secs";
			break;
		case 5:
			durationStr = (interval.toDurationMillis() / 1000) + " S";
			barSizeSetting = "5 secs";
			break;
		case 15:
			durationStr = (interval.toDurationMillis() / 1000) + " S";
			barSizeSetting = "15 secs";
			break;
		case 30:
			durationStr = (interval.toDurationMillis() / 1000) + " S";
			barSizeSetting = "30 secs";
			break;
		case 60:
			durationStr = (interval.toDurationMillis() / 1000) + " S";
			barSizeSetting = "1 min";
			break;
		case 120:
			durationStr = (interval.toDurationMillis() / 1000) + " S";
			barSizeSetting = "2 mins";
			break;
		case 180:
			durationStr = (interval.toDurationMillis() / 1000) + " S";
			barSizeSetting = "3 mins";
			break;
		case 300:
			durationStr = (interval.toDurationMillis() / 1000) + " S";
			barSizeSetting = "5 mins";
			break;
		case 900:
			durationStr = (interval.toDurationMillis() / 1000) + " S";
			barSizeSetting = "15 mins";
			break;
		case 1800:
			durationStr = (interval.toDurationMillis() / 1000) + " S";
			barSizeSetting = "30 mins";
			break;
		case 3600:
			durationStr = (interval.toDurationMillis() / 1000) + " S";
			barSizeSetting = "1 hour";
			break;
		case 86400:
			durationStr = (Days.daysIn(interval).getDays()) + " D";
			barSizeSetting = "1 day";
			break;
		default:
			throw new IllegalArgumentException("Invalid bar size: " + barSizeSeconds);
		}
		if (logger.isDebugEnabled())
			logger.debug("Requesting historical data: {} {} {} {}", new Object[] { symbol, endDateStr, durationStr, barSizeSetting });

		// Fetch TRADES
		int requestId = nextValidReqId.incrementAndGet();
		IBHistoricalRequest req = new IBHistoricalRequest(requestId, symbol, Duration.standardSeconds(barSizeSeconds));
		historicalRequestsById.put(requestId, req);
		socket.reqHistoricalData(requestId, makeContract(symbol), endDateStr, durationStr, barSizeSetting, symbol.isCash() ? "MIDPOINT" : "TRADES", 0, 2);
		long timeOut = System.currentTimeMillis() + historicalRequestTimeOutMillis;
		try {
			while (!req.done && !req.failed && timeOut > System.currentTimeMillis()) {
				Thread.sleep(500);
			}
		} catch (InterruptedException e) {
		}
		historicalRequestsById.remove(requestId);
		return req;
	}

	@Override
	public Tick getLastTick(Symbol symbol) {
		for (Iterator<Entry<Integer, IBTickRequest>> entries = tickRequestsById.entrySet().iterator(); entries.hasNext();) {
			Map.Entry<Integer, IBTickRequest> entry = entries.next();
			if (entry.getValue().symbol.equals(symbol)) {
				return entry.getValue().prevTick;
			}
		}
		return null;
	}

	@Override
	public Bar getLastBar(Symbol symbol) {
		for (Iterator<Entry<Integer, IBTickRequest>> entries = tickRequestsById.entrySet().iterator(); entries.hasNext();) {
			Map.Entry<Integer, IBTickRequest> entry = entries.next();
			if (entry.getValue().symbol.equals(symbol)) {
				return entry.getValue().prevBar;
			}
		}
		return null;
	}

	@Override
	public synchronized void addTickListener(TickListener listener) {
		logger.info("Adding TickListener {} for all symbols", listener.getClass().getSimpleName());
		tickListeners.add(listener);
	}

	@Override
	public synchronized void removeTickListener(TickListener listener) {
		boolean removed = tickListeners.remove(listener);
		if (removed) {
			logger.info("Removed TickListener {} for all symbols", listener.getClass().getSimpleName());
		}
		removeTickListener(null, listener);
	}

	@Override
	public void addTickListener(Symbol symbol, TickListener listener) {
		addTickListener(symbol, listener, false, null);
	}

	@Override
	public synchronized void addTickListener(Symbol symbol, TickListener listener, boolean marketDepth, Cleaner cleaner) {
		checkConnected();
		marketDepth = marketDepth && !symbol.isIndex();
		Integer reqId = null;
		IBTickRequest req = null;
		for (Iterator<Entry<Integer, IBTickRequest>> entries = tickRequestsById.entrySet().iterator(); entries.hasNext();) {
			Map.Entry<Integer, IBTickRequest> entry = entries.next();
			if (entry.getValue().symbol.equals(symbol)) {
				reqId = entry.getKey();
				req = entry.getValue();
				if (req.tickListeners.contains(listener)) {
					return;
				}
			}
		}
		if (marketDepth) {
			logger.info("Adding TickListener {} with marketdepth for symbol {} using cleaner {}",
					new Object[] { listener.getClass().getSimpleName(), symbol, cleaner });
		} else {
			logger.info("Adding TickListener {} for symbol {} using cleaner {}", new Object[] { listener.getClass().getSimpleName(), symbol, cleaner });
		}
		if (reqId == null) {
			Contract contract = makeContract(symbol);

			reqId = nextValidReqId.incrementAndGet();
			socket.reqContractDetails(reqId, contract);
			contractRequestsById.put(reqId, new IBContractRequest(reqId));

			reqId = nextValidReqId.incrementAndGet();
			socket.reqMktData(reqId, contract, "", false);
			if (marketDepth) {
				socket.reqMktDepth(reqId, contract, 10);
			}
			req = new IBTickRequest(reqId, symbol, marketDepth, cleaner);
			tickRequestsById.put(reqId, req);
		}
		req.tickListeners.add(listener);
	}

	@Override
	public synchronized void removeTickListener(Symbol symbol, TickListener listener) {
		for (Iterator<Entry<Integer, IBTickRequest>> entries = tickRequestsById.entrySet().iterator(); entries.hasNext();) {
			Map.Entry<Integer, IBTickRequest> entry = entries.next();
			if (symbol == null || entry.getValue().symbol.equals(symbol)) {
				boolean removed = entry.getValue().tickListeners.remove(listener);
				if (removed) {
					logger.info("Removed TickListener {} for symbol {}", listener.getClass().getSimpleName(), entry.getValue().symbol);
				}
				if (entry.getValue().tickListeners.isEmpty() && entry.getValue().barListeners.isEmpty()) {
					logger.info("Removed last TickListener for symbol {}, cancelling market data request {}", entry.getValue().symbol, entry.getValue());
					entries.remove();
					if (isConnected()) {
						if (entry.getValue().marketDepth) {
							socket.cancelMktDepth(entry.getKey());
						}
						socket.cancelMktData(entry.getKey());
					}
				}
			}
		}
	}

	@Override
	public synchronized void addBarListener(BarListener listener) {
		logger.info("Adding BarListener {} for all symbols", listener.getClass().getSimpleName());
		barListeners.add(listener);
	}

	@Override
	public synchronized void removeBarListener(BarListener listener) {
		boolean removed = barListeners.remove(listener);
		if (removed) {
			logger.info("Removed BarListener {} for all symbols", listener.getClass().getSimpleName());
		}
		removeBarListener(null, listener);
	}

	@Override
	public void addBarListener(Symbol symbol, BarListener listener) {
		addBarListener(symbol, listener, 60, null);
	}

	@Override
	public synchronized void addBarListener(Symbol symbol, BarListener listener, int barSize, Cleaner cleaner) {
		checkConnected();
		Integer reqId = null;
		IBTickRequest req = null;

		for (Iterator<Entry<Integer, IBTickRequest>> entries = tickRequestsById.entrySet().iterator(); entries.hasNext();) {
			Map.Entry<Integer, IBTickRequest> entry = entries.next();
			if (entry.getValue().symbol.equals(symbol)) {
				reqId = entry.getKey();
				req = entry.getValue();
				if (req.barSize != null && req.barSize.getStandardSeconds() != barSize) {
					throw new IllegalArgumentException("Existing listener uses different bar size: " + req.barSize.getStandardSeconds());
				}
				if (req.barListeners.contains(listener)) {
					return;
				}
			}
		}

		logger.info("Adding BarListener {} for symbol {}", listener.getClass().getSimpleName(), symbol);

		if (reqId == null) {
			Contract contract = makeContract(symbol);

			reqId = nextValidReqId.incrementAndGet();
			socket.reqContractDetails(reqId, contract);
			contractRequestsById.put(reqId, new IBContractRequest(reqId));

			reqId = nextValidReqId.incrementAndGet();
			// socket.reqRealTimeBars(reqId, contract, 5, "TRADES", false);
			socket.reqMktData(reqId, contract, "", false);
			req = new IBTickRequest(reqId, symbol, barSize, cleaner);

			tickRequestsById.put(reqId, req);
		} else {
			req.setBarSize(barSize);
		}
		req.barListeners.add(listener);
	}

	@Override
	public synchronized void removeBarListener(Symbol symbol, BarListener listener) {
		for (Iterator<Entry<Integer, IBTickRequest>> entries = tickRequestsById.entrySet().iterator(); entries.hasNext();) {
			Map.Entry<Integer, IBTickRequest> entry = entries.next();
			if (symbol == null || entry.getValue().symbol.equals(symbol)) {
				boolean removed = entry.getValue().barListeners.remove(listener);
				if (removed) {
					logger.info("Removed BarListener {} for symbol {}", listener.getClass().getSimpleName(), entry.getValue().symbol);
				}
				if (entry.getValue().tickListeners.isEmpty() && entry.getValue().barListeners.isEmpty()) {
					logger.info("Removed last BarListener for symbol {}, cancelling market data request {}", entry.getValue().symbol, entry.getValue());
					entries.remove();
					if (isConnected()) {
						socket.cancelMktData(entry.getKey());
					}
				}
			}
		}
	}

	@Override
	public synchronized void removeAllListeners() {
		super.removeAllListeners();
		barListeners.clear();
		tickListeners.clear();
		for (IBTickRequest req : new ArrayList<IBTickRequest>(tickRequestsById.values())) {
			for (BarListener listener : new ArrayList<BarListener>(req.barListeners)) {
				removeBarListener(null, listener);
			}
			for (TickListener listener : new ArrayList<TickListener>(req.tickListeners)) {
				removeTickListener(null, listener);
			}
		}
	}

	@Override
	public void historicalData(int reqId, String date, double open, double high, double low, double close, int volume, int count, double wap, boolean hasGaps) {
		try {
			if (logger.isDebugEnabled())
				logger.debug("historicalData: {} {} {} {} {} {} {} {} {} {}", new Object[] { reqId, date, open, low, high, close, volume, count, wap, hasGaps });

			IBHistoricalRequest req = historicalRequestsById.get(reqId);
			if (req == null) {
				socket.cancelHistoricalData(reqId);
				return;
			}
			req.timestamp = System.currentTimeMillis();

			if (date.startsWith("finished")) {
				req.done = true;
				return;
			}
			DateTime dateTime = null;
			if (req.barSize.getMillis() == 86400000) {
				dateTime = DateTimeFormat.forPattern("yyyyMMdd").parseDateTime(date);
			} else {
				dateTime = new DateTime(Long.parseLong(date) * 1000);
			}
			Bar bar = new Bar(req.barSize, req.symbol, dateTime, open, high, low, close, wap, volume, count);
			req.bars.put(bar.dateTime, bar);
		} catch (Throwable t) {
			// Do not allow exceptions come back to the socket -- it will cause
			// disconnects
			logger.error(t.getMessage(), t);
		}
	}

	@Override
	public void tickPrice(int reqId, int tickType, double price, int canAutoExecute) {
		tick(reqId, tickType, price, -1, null);
	}

	@Override
	public void tickSize(int reqId, int tickType, int size) {
		tick(reqId, tickType, -1, size, null);
	}

	@Override
	public void tickString(int reqId, int tickType, String value) {
		tick(reqId, tickType, -1, -1, value);
	}

	@Override
	public void tickGeneric(int reqId, int tickType, double value) {
		tick(reqId, tickType, value, -1, null);
	}

	private void tick(int reqId, int tickType, double doubleValue, int intValue, String stringValue) {
		try {
			if (logger.isDebugEnabled())
				logger.debug("tick: reqId: {}, tickType: {}, doubleValue: {}, intValue: {}, stringValue: {}", new Object[] { reqId, tickType, doubleValue, intValue,
						stringValue });

			IBTickRequest req = tickRequestsById.get(reqId);
			if (req != null) {
				buildTick(req, tickType, doubleValue, intValue, stringValue);
				if (req.barListeners.size() > 0) {
					buildBar(req, tickType, doubleValue, intValue, stringValue);
				}
			}
			if (req == null) {
				logger.warn("No matching request for tick event, cancelling market data: reqId: {}, tickType: {}, doubleValue: {}, intValue: {}, stringValue: {}",
						new Object[] { reqId, tickType, doubleValue, intValue, stringValue });
				socket.cancelMktData(reqId);
			}

		} catch (Throwable t) {
			// Do not allow exceptions come back to the socket -- it will cause
			// disconnects
			logger.error(t.getMessage(), t);
		}
	}

	private void buildBar(IBTickRequest req, int tickType, double doubleValue, int intValue, String stringValue) {
		try {
			synchronized (req) {
				Bar bar = req.currBar;
				long now = System.currentTimeMillis();
				if (bar == null) {
					bar = req.currBar = new Bar(req.barSize, req.symbol, new DateTime(now - (now % req.barSizeMillis)));
				} else if (bar.dateTime.getMillis() + req.barSizeMillis <= now) {
					boolean valid = isValidBar(req);
					if (!valid && req.prevBar != null) {
						bar.open = bar.high = bar.low = bar.close = bar.wap = req.prevBar.close;
						bar.volume = bar.trades = 0;
						valid = true;
					}
					if (valid) {
						int len = req.barListeners.size();
						for (int i = 0; i < len; i++) {
							BarListener listener = req.barListeners.get(i);
							try {
								listener.onBar(bar);
							} catch (Throwable t) {
								logger.error(t.getMessage(), t);
							}
						}
						len = barListeners.size();
						for (int i = 0; i < len; i++) {
							BarListener listener = barListeners.get(i);
							try {
								listener.onBar(bar);
							} catch (Throwable t) {
								logger.error(t.getMessage(), t);
							}
						}
						req.prevBar = bar;
						bar = req.currBar = new Bar(req.barSize, req.symbol, new DateTime(now - (now % req.barSizeMillis)));
					}
				}

				switch (tickType) {
				case TickType.BID:
				case TickType.ASK:
					// If currency then only bids and asks are received
					if (!req.symbol.isCash()) {
						break;
					}
				case TickType.LAST:
					if (bar.open == 0.0) {
						bar.open = doubleValue;
						bar.high = Double.NEGATIVE_INFINITY;
						bar.low = Double.POSITIVE_INFINITY;
					}
					bar.high = Math.max(bar.high, doubleValue);
					bar.low = Math.min(bar.low, doubleValue);
					bar.close = doubleValue;
					req.lastPrice = doubleValue;
					break;
				case TickType.VOLUME:
					if (intValue > 0) {
						// If new volume is lower than previous, reset has
						// occured.
						if (intValue < req.barVolume) {
							bar.volume = intValue;
							bar.wap = req.lastPrice;
						} else if (req.barVolume > 0) {
							int lastSize = (intValue - req.barVolume);
							bar.wap = Util.round((bar.wap * bar.volume + req.lastPrice * lastSize) / (bar.volume + lastSize), 2);
							bar.volume += lastSize;
							bar.trades++;
						} else {
							bar.volume = 0;
							bar.wap = req.lastPrice;
						}
						req.barVolume = intValue;
					}
					break;
				case TickType.LAST_TIMESTAMP:
					if (logger.isDebugEnabled()) {
						logger.debug("Tick timestamp {}", new DateTime(Long.parseLong(stringValue) * 1000));
					}
					// bar.dateTime = new DateTime(Long.parseLong(stringValue) *
					// 1000);
					break;
				case TickType.BID_SIZE:
				case TickType.ASK_SIZE:
				case TickType.OPEN:
				case TickType.CLOSE:
				case TickType.HIGH:
				case TickType.LOW:
				case TickType.LAST_SIZE:
				default:
					break;
				}
			}
		} catch (Throwable t) {
			// Do not allow exceptions come back to the socket -- it will cause
			// disconnects
			logger.error(t.getMessage(), t);
		}
	}

	private void buildTick(IBTickRequest req, int tickType, double doubleValue, int intValue, String stringValue) {
		try {
			// Ignore last size since it is not consistent, use volume change
			// instead.
			if (tickType == TickType.LAST_SIZE) {
				return;
			}
			req.timestamp = System.currentTimeMillis();

			Tick currTick = req.currTick;
			if (currTick == null) {
				currTick = req.currTick = new Tick(req.symbol);
			}
			currTick.dateTime = new DateTime(req.timestamp);
			currTick.lastSize = 0;

			switch (tickType) {
			case TickType.ASK:
				currTick.ask = doubleValue;
				break;
			case TickType.BID:
				currTick.bid = doubleValue;
				break;
			case TickType.LAST:
				currTick.price = doubleValue;
				break;
			// case TickType.OPEN:
			// break;
			// case TickType.CLOSE:
			// break;
			// case TickType.HIGH:
			// break;
			// case TickType.LOW:
			// break;
			case TickType.ASK_SIZE:
				currTick.askSize = intValue;
				break;
			case TickType.BID_SIZE:
				currTick.bidSize = intValue;
				break;
			// case TickType.LAST_SIZE:
			// Ignore last size since it is not consistent, use volume change
			// instead.
			// tick.lastSize = intValue;
			// break;
			case TickType.VOLUME:
				// If new volume is lower than previous, reset has occured.
				if (intValue > 0) {
					if (intValue < currTick.volume) {
						currTick.lastSize = intValue;
						currTick.volume = intValue;
					} else if (currTick.volume > 0) {
						currTick.lastSize = intValue - currTick.volume;
						currTick.volume = intValue;
					} else {
						currTick.lastSize = 0;
						currTick.volume = intValue;
					}
				}
				break;
			case TickType.LAST_TIMESTAMP:
				if (logger.isDebugEnabled()) {
					logger.debug("Tick timestamp {}", new DateTime(Long.parseLong(stringValue) * 1000));
				}
				// tick.dateTime = new DateTime(Long.parseLong(stringValue) *
				// 1000);
				break;
			}

			if (isValidTick(req)) {
				int len = req.tickListeners.size();
				for (int i = 0; i < len; i++) {
					TickListener listener = req.tickListeners.get(i);
					try {
						listener.onTick(currTick);
					} catch (Throwable t) {
						logger.error(t.getMessage(), t);
					}
				}
				len = tickListeners.size();
				for (int i = 0; i < len; i++) {
					TickListener listener = tickListeners.get(i);
					try {
						listener.onTick(currTick);
					} catch (Throwable t) {
						logger.error(t.getMessage(), t);
					}
				}
				req.prevTick = currTick;
				req.currTick = new Tick(currTick);
			}
		} catch (Throwable t) {
			// Do not allow exceptions come back to the socket -- it will cause
			// disconnects
			logger.error(t.getMessage(), t);
		}
	}

	private boolean isValidBar(IBBarRequest req) {
		Bar curr = req.currBar;
		if (curr.close <= 0 || curr.high <= 0 || curr.low <= 0 || curr.close <= 0) {
			return false;
		}
		if (curr.high < curr.low) {
			return false;
		}
		if (req.cleaner != null && Double.isNaN(req.cleaner.update(curr.getDateTime(), curr.getPrice()))) {
			return false;
		}
		return true;
	}

	private boolean isValidBar(IBTickRequest req) {
		Bar curr = req.currBar;
		if (curr.close <= 0 || curr.high <= 0 || curr.low <= 0 || curr.close <= 0) {
			return false;
		}
		if (curr.high < curr.low) {
			return false;
		}
		if (req.barCleaner != null && Double.isNaN(req.barCleaner.update(curr.getDateTime(), curr.getPrice()))) {
			return false;
		}
		return true;
	}

	private boolean isValidTick(IBTickRequest req) {
		Tick currTick = req.currTick;
		Tick prevTick = req.prevTick;
		if (req.marketDepth && currTick.marketDepth == null) {
			return false;
		}

		if (currTick.getSymbol().isIndex()) {
			if (currTick.price <= 0 && !currTick.getSymbol().isTickIndex()) {
				return false;
			}
			if (prevTick != null) {
				if (currTick.price == prevTick.price) {
					return false;
				}
			}
			return true;
		}
		if (currTick.askSize <= 0 || currTick.bidSize <= 0) {
			return false;
		}
		if (currTick.ask <= 0 || currTick.bid <= 0 || currTick.ask == currTick.bid) {
			return false;
		}
		if (prevTick != null) {
			if (currTick.ask == prevTick.ask && currTick.askSize == prevTick.askSize && currTick.bid == prevTick.bid && currTick.bidSize == prevTick.bidSize
					&& currTick.price == prevTick.price && currTick.lastSize == prevTick.lastSize && currTick.volume == prevTick.volume) {
				return false;
			}
		}
		if (req.tickCleaner != null && Double.isNaN(req.tickCleaner.update(currTick.dateTime, currTick.price))) {
			return false;
		}
		return true;
	}

	@Override
	public void updateMktDepth(int reqId, int position, int operation, int side, double price, int size) {
		try {
			if (logger.isDebugEnabled())
				logger.debug("reqId: {}, position: {}, operation: {}, side: {}, price: {}, size: {}", new Object[] { reqId, position, operation, side, price, size });

			if (price < 0) {
				return;
			}

			IBTickRequest req = tickRequestsById.get(reqId);
			if (req == null) {
				logger.warn(
						"No matching request for market depth event, cancelling market data: reqId: {}, position: {}, operation: {}, side: {}, price: {}, size: {}",
						new Object[] { reqId, position, operation, side, price, size });
				socket.cancelMktData(reqId);
				return;
			}
			if (req.currTick == null) {
				return;
			}

			MarketDepth md = req.currTick.marketDepth;
			if (md == null) {
				md = req.currTick.marketDepth = new MarketDepth(10);
			}
			double[] prices = (side == 0 ? md.askPrices : md.bidPrices); // ASK
			// 0,BID
			// 1
			int[] sizes = (side == 0 ? md.askSizes : md.bidSizes); // ASK 0,BID
			// 1

			switch (operation) {
			case 0: // INSERT
			case 1: // UPDATE
				prices[position] = price;
				sizes[position] = size;
				break;
			case 2: // DELETE
				prices[position] = 0;
				sizes[position] = 0;
				break;
			}
		} catch (Throwable t) {
			// Do not allow exceptions come back to the socket -- it will cause
			// disconnects
			logger.error(t.getMessage(), t);
		}
	}

	@Override
	public void realtimeBar(int reqId, long time, double open, double high, double low, double close, long volume, double wap, int count) {
		try {
			if (logger.isDebugEnabled())
				logger.debug("realtimeBar: reqId: {}, time: {}, open: {}, high: {}, low: {}, close: {}, volume: {}, wap: {}, count: {}", new Object[] { reqId, time,
						open, high, low, close, volume, wap, count });

			IBBarRequest req = barRequestsById.get(reqId);
			if (req == null) {
				socket.cancelRealTimeBars(reqId);
				barRequestsById.remove(reqId);
				return;
			}
			req.timestamp = System.currentTimeMillis();

			Bar bar = new Bar(realTimeBarSize, req.symbol, new DateTime(time * 1000), open, high, low, close, wap, (int) volume, count);
			if (isValidBar(req)) {
				for (BarListener listener : req.listeners) {
					try {
						listener.onBar(bar);
					} catch (Throwable t) {
						logger.error(t.getMessage(), t);
					}
				}
			}
			req.prevBar = bar;

		} catch (Throwable t) {
			// Do not allow exceptions come back to the socket -- it will cause
			// disconnects
			logger.error(t.getMessage(), t);
		}
	}

	@Override
	public void error(Exception e) {
		if (!connect && e.getMessage() != null && e.getMessage().startsWith("Socket closed")) {
			return;
		}
		logger.error(e.getMessage(), e);
	}

	@Override
	public void error(String error) {
		logger.error(error);
	}

	@Override
	public void error(int reqId, int errorCode, String errorMsg) {
		try {
			Object req = barRequestsById.get(reqId);
			if (req == null) {
				req = tickRequestsById.get(reqId);
			}
			if (req == null) {
				req = historicalRequestsById.get(reqId);
			}
			if (req == null) {
				req = scannerRequestsById.get(reqId);
			}
			if (req == null) {
				req = reqId;
			}
			String message = errorCode + ": " + errorMsg;
			lastMessage = message;

			if (req instanceof IBRequest) {
				((IBRequest) req).error = message;
			}

			switch (errorCode) {
			case 162:
				// Historical data request returned no data or pacing violation
				if (req instanceof IBHistoricalRequest) {
					logger.warn("Received error for request {}: {}", req, message);
					((IBHistoricalRequest) req).done = true;
				} else if (req instanceof IBScannerRequest) {
					logger.debug("Received error for request {}: {}", req, message);
					((IBScannerRequest) req).done = true;
				}
				break;
			case 200: // 200: bad contract
				logger.warn("Received error for request {}: {}", req, message);
				if (req instanceof IBHistoricalRequest) {
					((IBHistoricalRequest) req).failed = true;
				} else if (req instanceof IBScannerRequest) {
					((IBScannerRequest) req).done = true;
				}
				break;
			case 202: // 202: Cancelled order
				break;
			case 321: // 321: server error when validating an API client request
				logger.warn("Received error for request {}: {}", req, message);
				if (req instanceof IBHistoricalRequest) {
					((IBHistoricalRequest) req).failed = true;
				} else if (req instanceof IBScannerRequest) {
					((IBScannerRequest) req).done = true;
				}
				break;
			case 309: // 309: market depth requested for more than 3 symbols
				logger.warn("Received error for request {}: {}", req, message);
				// socket.cancelMktDepth(reqId);
				break;
			case 317: // 317: Market depth data has been reset
				logger.warn("Received error for request {}: {}", req, message);
				break;
			case 326:
				logger.info("Unable connect as the client id is already in use. Retry with a unique client id: {}", message);
				disconnect();
				break;
			case 399:
				logger.info("Received order message for request {}: {}", req, message);
				break;
			case 504:
				// Not connected
				logger.warn("Not connected: {}", message);
			case 1100:
				// Connectivity between IB and TWS has been lost.
				logger.warn("Connectivity between IB and TWS has been lost: {}", message);
				break;
			case 1101:
			case 1102:
			case 2100:
				logger.info("Received account message: {}", message);
				break;
			case 2104: // Market data farm connection is OK
			case 2106: // HMDS data farm connection is OK
			case 2107: // HMDS data farm connection is inactive but should be available upon demand
			case 2108: // Market data farm connection is inactive but should be available upon demand
			case 2119: // Market data farm is connecting
				//logger.info("Connectivity: {}", message);
				break;
			default:
				logger.warn("Received error for request {}: {}", req, message);
			}
		} catch (Throwable t) {
			// Do not allow exceptions come back to the socket -- it will cause
			// disconnects
			logger.error(t.getMessage(), t);
		}
	}

	public String getMarketScannerParameters() {
		// if (logger.isDebugEnabled())
		// logger.debug(String.format("Requesting historical data: %s %s %s %s",
		// symbol, endDateStr, durationStr, barSizeSetting));
		marketScannerParams = null;
		socket.reqScannerParameters();

		try {
			while (marketScannerParams == null) {
				Thread.sleep(500);
			}
		} catch (InterruptedException e) {
		}
		return marketScannerParams;
	}

	@Override
	public void scannerParameters(String xml) {
		marketScannerParams = xml;
	}

	public List<Symbol> scanMarket(ScannerType type, ScannerInstrument instrument, ScannerLocation location) {
		return scanMarket(type, instrument, location, false);
	}

	public List<Symbol> scanMarket(ScannerType type, ScannerInstrument instrument, ScannerLocation location, boolean exhaustive) {
		List<Symbol> result = new ArrayList<Symbol>();
		if (!exhaustive) {
			IBScannerRequest data = requestScannerSubscription(type, instrument, location, Double.MAX_VALUE, Double.MAX_VALUE);
			result.addAll(data.symbols);
			return result;
		}
		double increment = 1.0;
		double toPrice = 0;
		double fromPrice = 0;
		double maxPrice = 100000;
		while (true) {
			fromPrice = toPrice;
			toPrice = fromPrice + increment;
			IBScannerRequest data = requestScannerSubscription(type, instrument, location, fromPrice, toPrice);
			if (data.symbols.size() >= 50) {
				toPrice = fromPrice;
				increment /= 2;
				continue;
			}
			result.addAll(data.symbols);
			if (toPrice > maxPrice) {
				break;
			}
			if (data.symbols.size() < 25) {
				increment *= 2;
			}
		}
		return result;
	}

	public IBScannerRequest requestScannerSubscription(ScannerType type, ScannerInstrument instrument, ScannerLocation location, double minPrice, double maxPrice) {
		checkConnected();
		if (logger.isDebugEnabled())
			logger.debug("requestScannerSubscription: {} {} {} {} {}", new Object[] { type, instrument, location, minPrice, maxPrice });

		ScannerSubscription params = new ScannerSubscription();
		// params.scanCode("MOST_ACTIVE");
		// params.instrument("STOCK.EU");
		// params.locationCode("STK.EU.SFB");
		// params.scanCode("TOP_PERC_GAIN");
		// params.instrument("IND.US");
		// params.locationCode("IND.US");
		params.scanCode(type.toString());
		params.instrument(instrument.toString());
		params.locationCode(location.toString());
		params.abovePrice(minPrice);
		params.belowPrice(maxPrice);
		params.aboveVolume(0);
		params.averageOptionVolumeAbove(0);
		params.marketCapAbove(0);
		params.marketCapBelow(1.0E100);
		params.stockTypeFilter("ALL");

		int requestId = nextValidReqId.incrementAndGet();
		IBScannerRequest req = new IBScannerRequest(requestId);
		scannerRequestsById.put(requestId, req);
		socket.reqScannerSubscription(requestId, params);
		long timeOut = System.currentTimeMillis() + historicalRequestTimeOutMillis;
		try {
			while (!req.done && timeOut > System.currentTimeMillis()) {
				Thread.sleep(500);
			}
		} catch (InterruptedException e) {
		}
		scannerRequestsById.remove(requestId);
		return req;
	}

	@Override
	public void scannerData(int reqId, int rank, ContractDetails details, String distance, String benchmark, String projection, String legsStr) {
		if (logger.isDebugEnabled())
			logger.debug("scannerData: {} {} {} {} {} {} {}", new Object[] { reqId, rank, Util.toString(details.m_summary), Util.toString(details),
					distance, benchmark, projection, legsStr });
		try {
			IBScannerRequest req = scannerRequestsById.get(reqId);
			req.symbols.add(toSymbol(details.m_summary, details));
		} catch (Throwable t) {
			// Do not allow exceptions come back to the socket -- it will cause
			// disconnects
			logger.error(t.getMessage(), t);
		}
	}

	@Override
	public void scannerDataEnd(int reqId) {
		if (logger.isDebugEnabled())
			logger.debug("scannerDataEnd: {}", reqId);
		try {
			socket.cancelScannerSubscription(reqId);

			// IBScannerRequest req = scannerRequestsById.get(reqId);
			// req.done = true;

		} catch (Throwable t) {
			// Do not allow exceptions come back to the socket -- it will cause
			// disconnects
			logger.error(t.getMessage(), t);
		}
	}

	public List<Symbol> findSymbols(String code, String exchange, String currency, SymbolType type) {
		checkConnected();
		Contract contract = makeContract(new Symbol(code, exchange, currency, type));

		int reqId = nextValidReqId.incrementAndGet();
		IBContractRequest req = new IBContractRequest(reqId);
		contractRequestsById.put(reqId, req);

		socket.reqContractDetails(reqId, contract);

		long timeOut = System.currentTimeMillis() + historicalRequestTimeOutMillis;
		try {
			while (!req.done && timeOut > System.currentTimeMillis()) {
				Thread.sleep(500);
			}
		} catch (InterruptedException e) {
		}
		contractRequestsById.remove(reqId);
		return req.symbols;
	}

	@Override
	public void contractDetails(int reqId, ContractDetails contractDetails) {
		try {
			if (logger.isDebugEnabled())
				logger.debug("contractDetails: {} {}", reqId, Util.toString(contractDetails));

			IBContractRequest req = contractRequestsById.get(reqId);
			if (req == null) {
				return;
			}
			req.symbols.add(toSymbol(contractDetails.m_summary, contractDetails));
		} catch (Throwable t) {
			logger.error(t.getMessage(), t);
		}
	}

	@Override
	public void contractDetailsEnd(int reqId) {
		try {
			if (logger.isDebugEnabled())
				logger.debug("contractDetailsEnd: {}", reqId);

			IBContractRequest req = contractRequestsById.get(reqId);
			if (req == null) {
				return;
			}
			req.done = true;

		} catch (Throwable t) {
			logger.error(t.getMessage(), t);
		}
	}

	class IBRequest {
		int id;
		Symbol symbol;
		String error;
		long timestamp;

		IBRequest(int id, Symbol symbol) {
			this.id = id;
			this.symbol = symbol;
			this.timestamp = System.currentTimeMillis();
		}

		@Override
		public int hashCode() {
			return id;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj) {
				return true;
			}
			return obj != null && obj instanceof IBRequest && id == ((IBRequest) obj).id;
		}

		@Override
		public String toString() {
			StringBuilder sb = new StringBuilder();
			sb.append(getClass().getSimpleName());
			sb.append(" [id=");
			sb.append(id);
			sb.append(", ");
			if (symbol != null) {
				sb.append("symbol=");
				sb.append(symbol);
			}
			sb.append(", ts=");
			sb.append(new DateTime(timestamp));
			sb.append("]");
			return sb.toString();
		}
	}

	class IBTickRequest extends IBRequest {
		boolean marketDepth;
		Tick prevTick;
		Tick currTick;
		Cleaner tickCleaner;
		List<TickListener> tickListeners;

		long barSizeMillis;
		Duration barSize;
		int barVolume;
		double lastPrice;
		Bar prevBar;
		Bar currBar;
		Cleaner barCleaner;
		List<BarListener> barListeners;

		public IBTickRequest(int id, Symbol symbol, boolean marketDepth, Cleaner cleaner) {
			super(id, symbol);
			this.marketDepth = marketDepth;
			this.tickListeners = new ArrayList<TickListener>(4);
			this.barListeners = new ArrayList<BarListener>(4);
			this.tickCleaner = cleaner;
		}

		public IBTickRequest(int id, Symbol symbol, int barSizeSeconds, Cleaner cleaner) {
			super(id, symbol);
			this.barSizeMillis = barSizeSeconds * 1000;
			this.barSize = new Duration(barSizeMillis);
			this.tickListeners = new ArrayList<TickListener>(4);
			this.barListeners = new ArrayList<BarListener>(4);
			this.barCleaner = cleaner;
		}

		public void setBarSize(int barSize) {
			this.barSizeMillis = barSize * 1000;
			this.barSize = new Duration(barSizeMillis);
		}
	}

	class IBBarRequest extends IBRequest {
		Bar prevBar;
		Bar currBar;
		List<BarListener> listeners;
		Cleaner cleaner;

		public IBBarRequest(int id, Symbol symbol) {
			super(id, symbol);
			this.listeners = new ArrayList<BarListener>();
		}
	}

	class IBHistoricalRequest extends IBRequest {
		boolean failed;
		boolean done;
		NavigableMap<DateTime, Bar> bars;
		Duration barSize;

		public IBHistoricalRequest(int id, Symbol symbol, Duration barSize) {
			super(id, symbol);
			this.bars = new TreeMap<DateTime, Bar>();
			this.barSize = barSize;
		}
	}

	class IBScannerRequest {
		int id;
		boolean done;
		List<Symbol> symbols;
		int lastSize;

		public IBScannerRequest(int id) {
			this.id = id;
			this.symbols = new ArrayList<Symbol>();
		}

		@Override
		public int hashCode() {
			return id;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj) {
				return true;
			}
			return obj != null && obj instanceof IBScannerRequest && id == ((IBScannerRequest) obj).id;
		}

		@Override
		public String toString() {
			StringBuilder sb = new StringBuilder();
			sb.append(getClass().getSimpleName());
			sb.append(" [id=");
			sb.append(id);
			sb.append("]");
			return sb.toString();
		}
	}

	class IBContractRequest {
		int id;
		boolean done;
		List<Symbol> symbols;
		int lastSize;

		public IBContractRequest(int id) {
			this.id = id;
			this.symbols = new ArrayList<Symbol>();
		}

		@Override
		public int hashCode() {
			return id;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj) {
				return true;
			}
			return obj != null && obj instanceof IBContractRequest && id == ((IBContractRequest) obj).id;
		}

		@Override
		public String toString() {
			StringBuilder sb = new StringBuilder();
			sb.append(getClass().getSimpleName());
			sb.append(" [id=");
			sb.append(id);
			sb.append("]");
			return sb.toString();
		}
	}

	@Override
	public void connectionClosed() {
	}

	@Override
	public void tickEFP(int tickerId, int tickType, double basisPoints, String formattedBasisPoints, double impliedFuture, int holdDays, String futureExpiry,
			double dividendImpact, double dividendsToExpiry) {
	}

	@Override
	public void orderStatus(int orderId, String status, int filled, int remaining, double avgFillPrice, int permId, int parentId, double lastFillPrice,
			int clientId, String whyHeld) {
	}

	@Override
	public void openOrder(int orderId, Contract contract, Order order, OrderState orderState) {
	}

	@Override
	public void openOrderEnd() {
	}

	@Override
	public void updateAccountValue(String key, String value, String currency, String accountName) {
	}

	@Override
	public void updatePortfolio(Contract contract, int position, double marketPrice, double marketValue, double averageCost, double unrealizedPNL,
			double realizedPNL, String accountName) {
	}

	@Override
	public void updateAccountTime(String timeStamp) {
	}

	@Override
	public void accountDownloadEnd(String accountName) {
	}

	@Override
	public void nextValidId(int orderId) {
	}

	@Override
	public void bondContractDetails(int reqId, ContractDetails contractDetails) {
	}

	@Override
	public void execDetails(int reqId, Contract contract, Execution execution) {
	}

	@Override
	public void execDetailsEnd(int reqId) {
	}

	@Override
	public void updateMktDepthL2(int tickerId, int position, String marketMaker, int operation, int side, double price, int size) {
	}

	@Override
	public void updateNewsBulletin(int msgId, int msgType, String message, String origExchange) {
	}

	@Override
	public void managedAccounts(String accountsList) {
	}

	@Override
	public void receiveFA(int faDataType, String xml) {
	}

	@Override
	public void currentTime(long time) {
	}

	@Override
	public void fundamentalData(int reqId, String data) {
	}

	@Override
	public void deltaNeutralValidation(int reqId, UnderComp underComp) {
	}

	public enum ScannerType {
		TOP_PERC_GAIN, TOP_PERC_LOSE, MOST_ACTIVE, MOST_ACTIVE_USD, MOST_ACTIVE_AVG_USD, HALTED, HIGH_DIVIDEND_YIELD_IB, TOP_TRADE_COUNT, TOP_TRADE_RATE
	}

	public enum ScannerInstrument {
		STK, STOCK_EU, STOCK_HK, IND_EU, IND_US, IND_HK, FUT_EU, FUT_US, FUT_HK;

		@Override
		public String toString() {
			return super.toString().replace('_', '.');
		}
	}

	public enum ScannerLocation {
		IND_US, IND_EU, IND_HK, STK_US, STK_US_MAJOR, STK_NYSE, STK_NASDAQ, STK_AMEX, STK_ARCA, FUT_US, FUT_GLOBEX, FUT_ECBOT, FUT_IPE, FUT_NYBOT, FUT_NYMEX, FUT_NYSELIFFE, STK_EU, STK_EU_AEB, STK_EU_IBIS, STK_EU_LSE, STK_EU_SBF, STK_EU_SFB, STK_EU_VIRTX, FUT_EU, FUT_EU_BELFOX, FUT_EU_DTB, FUT_EU_FTA, FUT_EU_IDEM, FUT_EU_LIFFE, FUT_EU_MEFFRV, FUT_EU_MONEP;

		@Override
		public String toString() {
			return super.toString().replace('_', '.');
		}
	}

	@Override
	public void tickOptionComputation(int tickerId, int field, double impliedVol, double delta, double optPrice, double pvDividend, double gamma, double vega,
			double theta, double undPrice) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void tickSnapshotEnd(int reqId) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void marketDataType(int reqId, int marketDataType) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void commissionReport(CommissionReport commissionReport) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void position(String account, Contract contract, int pos, double avgCost) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void positionEnd() {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void accountSummary(int reqId, String account, String tag, String value, String currency) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void accountSummaryEnd(int reqId) {
		// TODO Auto-generated method stub
		
	}
}
