/*
* Copyright (c) 2012 Jeremy Goetsch
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jgoetsch.ib;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.ib.client.EClientSocket;
import com.ib.client.EWrapper;
import com.jgoetsch.ib.handlers.AccountDataHandler;
import com.jgoetsch.ib.handlers.AccountDataListenerHandler;
import com.jgoetsch.ib.handlers.ContractDetailsHandler;
import com.jgoetsch.ib.handlers.HandlerManager;
import com.jgoetsch.ib.handlers.HistoricalDataHandler;
import com.jgoetsch.ib.handlers.MarketDataHandler;
import com.jgoetsch.ib.handlers.MarketDataListenerHandler;
import com.jgoetsch.ib.handlers.MessageLogger;
import com.jgoetsch.ib.handlers.NextValidIdHandler;
import com.jgoetsch.ib.handlers.SimpleHandlerDelegatingWrapper;
import com.jgoetsch.tradeframework.Contract;
import com.jgoetsch.tradeframework.ContractDetails;
import com.jgoetsch.tradeframework.InvalidContractException;
import com.jgoetsch.tradeframework.NotConnectedException;
import com.jgoetsch.tradeframework.OHLC;
import com.jgoetsch.tradeframework.Order;
import com.jgoetsch.tradeframework.account.AccountData;
import com.jgoetsch.tradeframework.account.AccountDataListener;
import com.jgoetsch.tradeframework.account.AccountDataSource;
import com.jgoetsch.tradeframework.account.MultiAccountDataSource;
import com.jgoetsch.tradeframework.data.ContractDetailsSource;
import com.jgoetsch.tradeframework.data.DataUnavailableException;
import com.jgoetsch.tradeframework.data.HistoricalDataSource;
import com.jgoetsch.tradeframework.marketdata.MarketData;
import com.jgoetsch.tradeframework.marketdata.MarketDataListener;
import com.jgoetsch.tradeframework.marketdata.MarketDataSource;
import com.jgoetsch.tradeframework.order.ExecutionListener;
import com.jgoetsch.tradeframework.order.OrderException;
import com.jgoetsch.tradeframework.order.TradingService;
public class TWSService implements TradingService, AccountDataSource, MultiAccountDataSource, MarketDataSource, HistoricalDataSource, ContractDetailsSource {
private Logger log = LoggerFactory.getLogger(TWSService.class);
protected final HandlerManager handlerManager;
protected final EClientSocket eClientSocket;
protected final Map<Contract, MarketDataListenerHandler> marketDataSubscriptions = new HashMap<Contract, MarketDataListenerHandler>();
protected final Map<String, AccountDataListenerHandler> accountDataSubscriptions = new HashMap<String, AccountDataListenerHandler>();
private int curRequestId = -1;
private String host = "localhost";
private int port = 7496;
private int clientId = 1;
public TWSService() {
super();
handlerManager = new SimpleHandlerDelegatingWrapper();
handlerManager.addHandler(new MessageLogger());
eClientSocket = new EClientSocket((EWrapper)handlerManager);
}
public TWSService(String host, int port, int clientid) throws NotConnectedException {
super();
handlerManager = new SimpleHandlerDelegatingWrapper();
handlerManager.addHandler(new MessageLogger());
eClientSocket = new EClientSocket((EWrapper)handlerManager);
if (!connect(host, port, clientid))
throw new NotConnectedException();
}
public boolean connect() {
NextValidIdHandler h = new NextValidIdHandler();
handlerManager.addHandler(h);
boolean success;
synchronized (h) {
eClientSocket.eConnect(host, port, clientId);
success = h.block();
curRequestId = h.getId();
}
handlerManager.removeHandler(h);
if (success)
log.info("Connected to TWS at " + host + ":" + port + " as clientId " + clientId + ", initial order id = " + curRequestId);
else
log.warn("Failed to connect to TWS at " + host + ":" + port + " as clientId " + clientId);
return success;
}
public boolean connect(String host, int port, int clientId) {
this.host = host;
this.port = port;
this.clientId = clientId;
return connect();
}
public void close() {
if (eClientSocket.isConnected()) {
eClientSocket.eDisconnect();
}
}
public boolean isConnected() {
return eClientSocket.isConnected();
}
protected synchronized int getNextId() {
return curRequestId++;
}
public void placeOrder(Contract contract, Order order) throws InvalidContractException, OrderException {
com.ib.client.Contract twsContract = TWSUtils.toTWSContract(contract);
com.ib.client.Order twsOrder = TWSUtils.toTWSOrder(order);
eClientSocket.placeOrder(getNextId(), twsContract, twsOrder);
}
/*
* Account data
*/
public double getAccountValue(String valueType) {
return getAccountValue(valueType, "");
}
public double getAccountValue(String valueType, String acctCode) {
AccountData acctData = getAccountDataSnapshot(acctCode);
return acctData != null ? acctData.getValue(valueType) : 0;
}
public AccountData getAccountDataSnapshot() {
return getAccountDataSnapshot("");
}
public synchronized AccountData getAccountDataSnapshot(String accountCode) {
AccountDataHandler v = new AccountDataHandler();
long startTime = System.currentTimeMillis();
handlerManager.addHandler(v);
eClientSocket.reqAccountUpdates(true, accountCode);
boolean success;
synchronized (v) {
success = v.block();
}
eClientSocket.reqAccountUpdates(false, accountCode);
if (log.isDebugEnabled() && success)
log.debug("Received account data snapshot in " + (System.currentTimeMillis() - startTime) + " ms");
else if (log.isWarnEnabled() && !success)
log.warn("Timeout receiving account data snapshot in " + (System.currentTimeMillis() - startTime) + " ms");
handlerManager.removeHandler(v);
return v.getNetLiquidationValue() > 0 ? v : null;
}
public void subscribeAccountData(AccountDataListener listener) {
subscribeAccountData(listener, "");
}
public void subscribeAccountData(AccountDataListener listener, String accountCode) {
synchronized (accountDataSubscriptions) {
AccountDataListenerHandler accountDataSubscription = accountDataSubscriptions.get(accountCode);
if (accountDataSubscription == null) {
accountDataSubscription = new AccountDataListenerHandler(accountCode);
accountDataSubscriptions.put(accountCode, accountDataSubscription);
}
boolean subscribe = !accountDataSubscription.hasListeners();
accountDataSubscription.addListener(listener);
if (subscribe) {
handlerManager.addHandler(accountDataSubscription);
eClientSocket.reqAccountUpdates(true, "");
}
}
}
public void cancelAccountDataSubscription(AccountDataListener listener) {
cancelAccountDataSubscription(listener, "");
}
public void cancelAccountDataSubscription(AccountDataListener listener, String accountCode) {
synchronized (accountDataSubscriptions) {
AccountDataListenerHandler accountDataSubscription = accountDataSubscriptions.get(accountCode);
if (accountDataSubscription == null || !accountDataSubscription.removeListener(listener))
throw new IllegalArgumentException("Attempted to cancel account " + accountCode + " data for listener that is not subscribed: " + listener);
else {
if (!accountDataSubscription.hasListeners()) {
eClientSocket.reqAccountUpdates(false, accountCode);
handlerManager.removeHandler(accountDataSubscription);
accountDataSubscriptions.remove(accountCode);
}
}
}
}
public MarketData getMktDataSnapshot(Contract contract) throws InvalidContractException {
int tickerId = getNextId();
MarketDataHandler mkd = new MarketDataHandler(tickerId);
long startTime = System.currentTimeMillis();
handlerManager.addHandler(mkd);
boolean success;
synchronized (mkd) {
eClientSocket.reqMktData(tickerId, TWSUtils.toTWSContract(contract), null, true);
success = mkd.block();
}
handlerManager.removeHandler(mkd);
if (log.isDebugEnabled() && success)
log.debug("Received market data snapshot for " + contract + " in " + (System.currentTimeMillis() - startTime) + " ms");
else if (log.isWarnEnabled() && !success)
log.warn("Timeout waiting for market data snapshot for " + contract + " in " + (System.currentTimeMillis() - startTime) + " ms");
if (!success && (mkd.getErrorCode() == 200 || mkd.getErrorCode() == 203))
throw new InvalidContractException(contract, mkd.getErrorMsg());
return mkd;
}
public MarketData getDataSnapshot(Contract contract) throws IOException, InvalidContractException, DataUnavailableException {
return getMktDataSnapshot(contract);
}
public void subscribeMarketData(Contract contract, MarketDataListener marketDataListener) {
int tickerId = getNextId();
synchronized (marketDataSubscriptions) {
MarketDataListenerHandler mkdlHandler = marketDataSubscriptions.get(contract);
if (mkdlHandler == null) {
mkdlHandler = new MarketDataListenerHandler(tickerId, contract);
marketDataSubscriptions.put(contract, mkdlHandler);
handlerManager.addHandler(mkdlHandler);
eClientSocket.reqMktData(tickerId, TWSUtils.toTWSContract(contract), null, false);
}
mkdlHandler.addListener(marketDataListener);
}
}
public void cancelMarketData(Contract contract, MarketDataListener marketDataListener) {
synchronized (marketDataSubscriptions) {
MarketDataListenerHandler mkdlHandler = marketDataSubscriptions.get(contract);
if (mkdlHandler == null || !mkdlHandler.removeListener(marketDataListener))
throw new IllegalArgumentException("Attempted to cancel market data for listener that is not subscribed: " + contract + ", " + marketDataListener);
else {
if (!mkdlHandler.hasListeners()) {
eClientSocket.cancelMktData(mkdlHandler.getId());
handlerManager.removeHandler(mkdlHandler);
marketDataSubscriptions.remove(contract);
}
}
}
}
public ContractDetails getContractDetails(Contract contract) throws IOException, InvalidContractException {
int tickerId = getNextId();
ContractDetailsHandler mkd = new ContractDetailsHandler(tickerId);
long startTime = System.currentTimeMillis();
handlerManager.addHandler(mkd);
boolean success;
synchronized (mkd) {
eClientSocket.reqContractDetails(tickerId, TWSUtils.toTWSContract(contract));
success = mkd.block();
}
handlerManager.removeHandler(mkd);
if (log.isDebugEnabled() && success)
log.debug("Received contract details for " + contract + " in " + (System.currentTimeMillis() - startTime) + " ms");
else if (log.isWarnEnabled() && !success)
log.warn("Timeout waiting for contract details for " + contract + " in " + (System.currentTimeMillis() - startTime) + " ms");
if (!success && (mkd.getErrorCode() == 200 || mkd.getErrorCode() == 203))
throw new InvalidContractException(contract, mkd.getErrorMsg());
return mkd.getContractDetails();
}
/*
* Historical Data
*/
private static final String histPeriodUnit[] = { "1 sec", "5 secs", "15 secs", "30 secs", "1 min", "2 mins", "3 mins", "5 mins", "15 mins", "30 mins", "1 hour", "1 day", "1 week", "1 month", "3 months", "1 year" };
private static final String histDurationUnit[] = { "S", "S", "S", "S", "S", "S", "S", "S", "S", "S", "S", "D", "W", "M", "M", "Y" };
private static final int histDurationMultiplier[] = { 1, 5, 15, 30, 60, 120, 180, 300, 900, 1800, 3600, 1, 1, 1, 3, 1 };
private Queue<Long> historicalReqs = new LinkedList<Long>();
public OHLC[] getHistoricalData(Contract contract, Date endDate, int numPeriods, int periodUnit) throws InvalidContractException, DataUnavailableException
{
String duration = (numPeriods * histDurationMultiplier[periodUnit]) + " " + histDurationUnit[periodUnit];
return getHistoricalData(contract, endDate, duration, periodUnit, true);
}
public OHLC[] getHistoricalData(Contract contract, Date endDate, String duration, int periodUnit, boolean onlyRTH) throws InvalidContractException, DataUnavailableException
{
if (!isConnected())
throw new DataUnavailableException("TWS service is not connected");
DateFormat df = new SimpleDateFormat("yyyyMMdd HH:mm:ss");
df.setTimeZone(HistoricalDataSource.timeZone);
log.debug("getHistoricalData: " + contract + " " + duration + ", " + histPeriodUnit[periodUnit] + " ending " + df.format(new Date(endDate.getTime() - 1)));
boolean success, retry;
HistoricalDataHandler hdh;
do {
int tickerId = getNextId();
hdh = new HistoricalDataHandler(tickerId);
handlerManager.addHandler(hdh);
eClientSocket.reqHistoricalData(tickerId, TWSUtils.toTWSContract(contract), df.format(new Date(endDate.getTime() - 1)) + " EST", duration, histPeriodUnit[periodUnit], "TRADES", onlyRTH ? 1 : 0, 2);
synchronized (hdh) {
success = hdh.block();
}
retry = !success && hdh.getErrorMsg() != null && hdh.getErrorMsg().indexOf("pacing violation") != -1;
if (retry) {
log.info("Pacing violation, waiting to retry data request...");
try {
Thread.sleep(15000);
} catch (InterruptedException e) { }
}
} while (retry);
if (!success && (hdh.getErrorCode() == 200 || hdh.getErrorCode() == 203 || hdh.getErrorCode() == 162))
throw new InvalidContractException(contract, hdh.getErrorMsg());
else if (!success)
log.warn(hdh.getErrorCode() + ": " + hdh.getErrorMsg());
return success ? hdh.getData() : null;
}
@Override
public String toString() {
return "Live TWSService " + (isConnected() ? "connected, " : "disconnected, ") + handlerManager.getHandlers().size() + " handlers";
}
public void cancelExecutionSubscription(ExecutionListener listener) {
// TODO Auto-generated method stub
}
public void subscribeExecutions(ExecutionListener listener) {
// TODO Auto-generated method stub
}
public void setHost(String host) {
this.host = host;
}
public String getHost() {
return host;
}
public void setPort(int port) {
this.port = port;
}
public int getPort() {
return port;
}
public void setClientId(int clientid) {
this.clientId = clientid;
}
public int getClientId() {
return clientId;
}
}