package jtrade.trader;
import java.io.File;
import java.util.ArrayList;
import java.util.Currency;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import jtrade.JTradeException;
import jtrade.Symbol;
import jtrade.SymbolFactory;
import jtrade.marketfeed.IBMarketFeed;
import jtrade.marketfeed.Tick;
import jtrade.util.Configurable;
import jtrade.util.Util;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MarkerFactory;
import com.ib.client.Contract;
import com.ib.client.Execution;
import com.ib.client.Order;
import com.ib.client.OrderState;
public class IBTrader extends IBMarketFeed implements Trader {
private static final Logger logger = LoggerFactory.getLogger(IBTrader.class);
private static final Logger blotter = LoggerFactory.getLogger("blotter");
public final Configurable<String> ACCOUNT_CODE = new Configurable<String>("ACCOUNT_CODE", "");
public final Configurable<Boolean> REAL_MONEY_ACCOUNT = new Configurable<Boolean>("REAL_MONEY_ACCOUNT", false);
protected AtomicInteger nextValidOrderId;
protected String accountCode;
protected Map<Integer, OpenOrder> openOrdersById;
protected List<OrderListener> orderListeners;
protected Portfolio portfolio;
protected Commission commission;
protected PerformanceTracker performanceTracker;
public IBTrader() {
this((String) null, -1, (File) null);
}
public IBTrader(int clientId) {
this((String) null, clientId, (File) null);
}
public IBTrader(String host, int clientId) {
this(host, clientId, (File) null);
}
public IBTrader(String host, int clientId, String dataDir) {
this(host, clientId, new File(dataDir));
}
public IBTrader(String host, int clientId, File dataDir) {
super(host, clientId);
openOrdersById = new HashMap<Integer, OpenOrder>();
orderListeners = new ArrayList<OrderListener>();
portfolio = new Portfolio(this);
performanceTracker = new PerformanceTracker(this);
}
@Override
protected synchronized void doConnect() {
super.doConnect();
if (isConnected()) {
logger.info("Requesting account updates for {}", ACCOUNT_CODE.get());
socket.reqAccountUpdates(true, ACCOUNT_CODE.get());
socket.reqOpenOrders();
try {
while (accountCode == null) {
wait();
}
} catch (InterruptedException e) {
throw new JTradeException(e);
}
commission = new Commission(0.0, 0.0, 0.0, 0.0, 0.0);
if (!ACCOUNT_CODE.get().equals(accountCode)) {
logger.info("Account code does not match specified account {} <> {}, exiting.", accountCode, ACCOUNT_CODE.get());
disconnect();
} else if (!accountCode.startsWith("D")) {
if (!REAL_MONEY_ACCOUNT.get()) {
logger.info("Connected to real money account {} without #REAL_MONEY_ACCOUNT set to true, exiting.", accountCode);
disconnect();
}
logger.info("Connected to real money account {} (Manage risk accordingly!)", accountCode);
} else {
logger.info("Connected to paper money account {}", accountCode);
}
}
}
@Override
public synchronized void addOrderListener(OrderListener listener) {
logger.info("Adding OrderListener {}", listener.getClass().getSimpleName());
orderListeners.add(listener);
}
@Override
public synchronized void removeOrderListener(OrderListener listener) {
boolean removed = orderListeners.remove(listener);
if (removed) {
logger.info("Removed OrderListener {}", listener.getClass().getSimpleName());
}
}
@Override
public synchronized void removeAllListeners() {
super.removeAllListeners();
orderListeners.clear();
}
protected Order makeOrder(OpenOrder openOrder) {
Order order = new Order();
order.m_orderRef = openOrder.getReference() != null ? openOrder.getReference() : "";
order.m_overridePercentageConstraints = true;
order.m_totalQuantity = Math.abs(openOrder.getQuantity());
if (openOrder.isBuy()) {
order.m_action = "BUY";
} else if (openOrder.isSell()) {
order.m_action = "SELL";
}
if (openOrder.isMarket()) {
order.m_orderType = "MKT";
} else if (openOrder.isLimit()) {
order.m_orderType = "LMT";
order.m_lmtPrice = openOrder.getPrice();
} else if (openOrder.isStopMarket()) {
order.m_orderType = "STP";
order.m_triggerMethod = 8; // midpoint
order.m_auxPrice = openOrder.getStopPrice();
} else if (openOrder.isStopLimit()) {
order.m_orderType = "STPLMT";
order.m_triggerMethod = 8; // midpoint
order.m_lmtPrice = openOrder.getPrice();
order.m_auxPrice = openOrder.getStopPrice();
} else if (openOrder.isTrailMarket()) {
order.m_orderType = "TRAIL";
order.m_triggerMethod = 8; // midpoint
order.m_auxPrice = openOrder.getTrailStopOffset();
} else if (openOrder.isTrailLimit()) {
order.m_orderType = "TRAILLMT";
order.m_triggerMethod = 8; // midpoint
order.m_lmtPrice = openOrder.getPrice();
order.m_trailStopPrice = openOrder.getStopPrice();
order.m_auxPrice = openOrder.getTrailStopOffset();
}
return order;
}
protected OpenOrder toOpenOrder(Contract contract, Order order) {
int orderId = order.m_orderId;
int quantity = order.m_totalQuantity;
if ("SELL".equals(order.m_action)) {
quantity = -quantity;
}
OrderType type = null;
double price = -1;
double stopPrice = -1;
double trailStopOffset = -1;
if ("MKT".equals(order.m_orderType)) {
type = OrderType.MARKET;
} else if ("LMT".equals(order.m_orderType)) {
type = OrderType.LIMIT;
price = order.m_lmtPrice;
} else if ("STP".equals(order.m_orderType)) {
type = OrderType.STOP_MARKET;
stopPrice = order.m_auxPrice;
} else if ("STPLMT".equals(order.m_orderType)) {
type = OrderType.STOP_LIMIT;
price = order.m_lmtPrice;
stopPrice = order.m_auxPrice;
} else if ("TRAIL".equals(order.m_orderType)) {
type = OrderType.TRAIL_MARKET;
order.m_triggerMethod = 8; // midpoint
stopPrice = order.m_trailStopPrice;
trailStopOffset = order.m_auxPrice;
} else if ("TRAILLMT".equals(order.m_orderType)) {
type = OrderType.TRAIL_LIMIT;
price = order.m_lmtPrice;
stopPrice = order.m_trailStopPrice;
trailStopOffset = order.m_auxPrice;
}
OpenOrder openOrder = new OpenOrder(orderId, toSymbol(contract), type, quantity, price, stopPrice, trailStopOffset, new DateTime(), order.m_orderRef);
return openOrder;
}
@Override
public List<OpenOrder> getOpenOrders() {
return new ArrayList<OpenOrder>(openOrdersById.values());
}
@Override
public List<OpenOrder> getOpenOrders(Symbol symbol, OrderType orderType) {
List<OpenOrder> openOrders = new ArrayList<OpenOrder>(openOrdersById.size());
for (OpenOrder o : openOrdersById.values()) {
if ((symbol == null || symbol.equals(o.getSymbol())) && (orderType == null || orderType.equals(o.getType()))) {
openOrders.add(o);
}
}
return openOrders;
}
@Override
public OpenOrder getOpenOrder(Symbol symbol, OrderType orderType) {
if (symbol == null) {
throw new IllegalArgumentException("Symbol cannot be null");
}
if (orderType == null) {
throw new IllegalArgumentException("OrderType cannot be null");
}
for (OpenOrder o : openOrdersById.values()) {
if (symbol.equals(o.getSymbol()) && orderType.equals(o.getType())) {
return o;
}
}
return null;
}
@Override
public synchronized OpenOrder placeOrder(Symbol symbol, int quantity, String reference) {
return placeOrder(symbol, OrderType.MARKET, quantity, -1, -1, reference);
}
@Override
public synchronized OpenOrder placeOrder(Symbol symbol, int quantity, double price, String reference) {
return placeOrder(symbol, OrderType.LIMIT, quantity, price, -1, reference);
}
@Override
public synchronized OpenOrder placeOrder(Symbol symbol, OrderType type, int quantity, double price, double stopPercent, String reference) {
if (quantity == 0) {
throw new IllegalArgumentException(String.format("Invalid quantity %s", quantity));
}
if ((OrderType.LIMIT.equals(type) || OrderType.STOP_LIMIT.equals(type) || OrderType.TRAIL_LIMIT.equals(type)) && price <= 0) {
throw new IllegalArgumentException(String.format("Invalid limit order price %s", price));
}
if ((OrderType.STOP_MARKET.equals(type) || OrderType.STOP_LIMIT.equals(type) || OrderType.TRAIL_MARKET.equals(type) || OrderType.TRAIL_LIMIT.equals(type))
&& stopPercent <= 0) {
throw new IllegalArgumentException(String.format("Invalid stop order with stop percent %s", stopPercent));
}
checkConnected();
OpenOrder openOrder = getOpenOrder(symbol, type);
if (openOrder != null && !openOrder.isFilled() && !openOrder.isCancelled()) {
throw new IllegalStateException("OpenOrder for same strategy, symbol and type already exists: " + openOrder);
}
DateTime now = new DateTime();
double stopPrice = -1;
double trailingStopOffset = -1;
if (OrderType.STOP_MARKET.equals(type) || OrderType.STOP_LIMIT.equals(type) || OrderType.TRAIL_MARKET.equals(type) || OrderType.TRAIL_LIMIT.equals(type)) {
Tick t = getLastTick(symbol);
if (t == null) {
t = getLastTick(symbol, now);
if (t == null) {
throw new IllegalStateException(String.format("Cannot set stop without tick data for symbol %s", symbol));
}
}
trailingStopOffset = Util.round(t.getMidPrice() * stopPercent, 10.0);
if (quantity < 0) {
stopPrice = t.getMidPrice() - trailingStopOffset;
} else {
stopPrice = t.getMidPrice() + trailingStopOffset;
}
stopPrice = Util.round(stopPrice, 2.0);
}
int orderId = nextValidOrderId.getAndIncrement();
openOrder = new OpenOrder(orderId, symbol, type, quantity, price, stopPrice, trailingStopOffset, new DateTime(), reference);
openOrdersById.put(orderId, openOrder);
Contract contract = makeContract(symbol);
Order order = makeOrder(openOrder);
logger.info("Placing order {}", openOrder);
socket.placeOrder(orderId, contract, order);
for (OrderListener listener : orderListeners) {
try {
listener.onOrderPlaced(openOrder);
} catch (Throwable t) {
logger.error(t.getMessage(), t);
}
}
return openOrder;
}
@Override
public synchronized void cancelOrder(Symbol symbol, OrderType orderType) {
List<OpenOrder> openOrders = getOpenOrders(symbol, orderType);
if (openOrders.isEmpty()) {
logger.info("Cannot cancel order, no open order found for {} {}", new Object[] { symbol, orderType });
return;
}
for (OpenOrder o : openOrders) {
cancelOrder(o);
}
}
@Override
public synchronized void cancelOrder(OpenOrder openOrder) {
checkConnected();
socket.cancelOrder(openOrder.getOrderId());
}
@Override
public void cancelOrders() {
for (OpenOrder o : openOrdersById.values()) {
cancelOrder(o);
}
}
protected void logExecution(OpenOrder openOrder, int quantity) {
Object[] params = new Object[] { openOrder.getFillDate(), "EXEC", openOrder.getAction(), openOrder.getType(), quantity, openOrder.getSymbol(),
openOrder.getSymbol().getCurrency(), openOrder.getLastFillPrice(), "", "", "", "", "",
openOrder.getReference() != null ? openOrder.getReference() : "", accountCode };
blotter.info(MarkerFactory.getMarker("EXECUTION"), "{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}", params);
}
protected void logTrade(OpenOrder openOrder, int position, double costBasis, double realized, double unrealized) {
Object[] params = new Object[] { openOrder.getFillDate(), "TRADE", openOrder.getAction(), openOrder.getType(), openOrder.getQuantityFilled(),
openOrder.getSymbol(), openOrder.getSymbol().getCurrency(), openOrder.getAvgFillPrice(), position, Util.round(costBasis, 4),
Util.round(realized, 4), Util.round(unrealized, 4), Util.round(openOrder.getCommission(), 4),
openOrder.getReference() != null ? openOrder.getReference() : "", accountCode };
blotter.info(MarkerFactory.getMarker("TRADE"), "{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}", params);
}
private void closeOpenOrder(OpenOrder openOrder, double commissionAmount) {
openOrdersById.remove(openOrder.getOrderId());
if (Double.isNaN(commissionAmount) || commissionAmount <= 0.0) {
commissionAmount = commission.calculate(openOrder.getQuantityFilled(), openOrder.getAvgFillPrice());
}
openOrder.setCommission(commissionAmount);
logger.info("{} {}", openOrder.isFilled() ? "Filled" : "Partially filled", openOrder);
Position position = portfolio.getPosition(openOrder.getSymbol());
double[] values = position.update(openOrder.getFillDate(), openOrder.getQuantityFilled(), openOrder.getAvgFillPrice(), openOrder.getCommission());
logTrade(openOrder, position.getQuantity(), values[0], values[1], values[2]);
for (OrderListener listener : orderListeners) {
try {
listener.onOrderFilled(openOrder, position);
} catch (Throwable t) {
logger.error(t.getMessage(), t);
}
}
}
@Override
public void nextValidId(int nextValidOrderId) {
try {
if (logger.isDebugEnabled())
logger.debug("nextValidId: {}", nextValidOrderId);
this.nextValidOrderId = new AtomicInteger(nextValidOrderId);
} catch (Throwable t) {
logger.error(t.getMessage(), t);
}
}
@Override
public void execDetails(int reqId, Contract contract, Execution execution) {
try {
if (logger.isDebugEnabled())
logger.debug("execDetails: {} {} {}", new Object[] { reqId, Util.toString(contract), Util.toString(execution) });
OpenOrder openOrder = openOrdersById.get(execution.m_orderId);
DateTime dt = DateTimeFormat.forPattern("yyyyMMdd HH:mm:ss").parseDateTime(execution.m_time);
if (openOrder != null) {
int quantityChange = openOrder.isBuy() ? execution.m_shares : -execution.m_shares;
openOrder.update(quantityChange, execution.m_price, dt);
logExecution(openOrder, quantityChange);
} else {
logger.info("Execution does not match any open order {} {}", Util.toString(contract), Util.toString(execution));
}
} catch (Throwable t) {
// Do not allow exceptions come back to the socket -- it will cause
// disconnects
logger.error(t.getMessage(), t);
}
}
@Override
public void execDetailsEnd(int reqId) {
try {
if (logger.isDebugEnabled())
logger.debug("execDetailsEnd: {}", reqId);
} catch (Throwable t) {
// Do not allow exceptions come back to the socket -- it will cause
// disconnects
logger.error(t.getMessage(), t);
}
}
@Override
public void orderStatus(int orderId, String status, int filled, int remaining, double avgFillPrice, int permId, int parentId, double lastFillPrice,
int clientId, String whyHeld) {
try {
if (logger.isDebugEnabled())
logger.debug("orderStatus: {} {} {} {} {} {} {} {} {} {}", new Object[] { orderId, status, filled, remaining, avgFillPrice, permId, parentId,
lastFillPrice, clientId, whyHeld });
if ("Cancelled".equals(status)) {
OpenOrder openOrder = openOrdersById.remove(orderId);
if (openOrder != null) {
openOrder.setCancelled();
logger.info("Cancelled {}", openOrder);
for (OrderListener listener : orderListeners) {
try {
listener.onOrderCancelled(openOrder);
} catch (Throwable t) {
logger.error(t.getMessage(), t);
}
}
if ((openOrder.isFilled() || (!openOrder.isOpen() && openOrder.getQuantityFilled() > 0))) {
closeOpenOrder(openOrder, 0);
}
} else {
logger.warn("Cancelled order {}, no matching order found!", orderId);
}
}
} catch (Throwable t) {
// Do not allow exceptions come back to the socket -- it will cause
// disconnects
logger.error(t.getMessage(), t);
}
}
@Override
public void openOrder(int orderId, Contract contract, Order order, OrderState orderState) {
try {
if (logger.isDebugEnabled())
logger.debug("openOrder: {} {} {} {}",
new Object[] { orderId, Util.toString(contract), Util.toString(order), Util.toString(orderState) });
OpenOrder openOrder = openOrdersById.get(orderId);
if (openOrder != null) {
if ((openOrder.isFilled() || (!openOrder.isOpen() && openOrder.getQuantityFilled() > 0)) && orderState.m_commission != Double.MAX_VALUE) {
closeOpenOrder(openOrder, orderState.m_commission);
}
} else {
if ("PendingSubmit".equals(orderState.m_status) || "PreSubmitted".equals(orderState.m_status) || "Submitted".equals(orderState.m_status)) {
openOrder = toOpenOrder(contract, order);
openOrdersById.put(orderId, openOrder);
logger.info("Added {}", openOrder);
}
}
} catch (Throwable t) {
logger.error(t.getMessage(), t);
}
}
@Override
public void openOrderEnd() {
try {
if (logger.isDebugEnabled())
logger.debug("openOrderEnd");
} catch (Throwable t) {
logger.error(t.getMessage(), t);
}
}
@Override
public void updateAccountValue(String key, String value, String currency, String accountName) {
try {
if (logger.isDebugEnabled())
logger.debug("updateAccountValue: {} {} {} {}", new Object[] { key, value, currency, accountName });
if ("AccountCode".equals(key)) {
synchronized (this) {
this.accountCode = value;
notifyAll();
}
} else if ("AvailableFunds".equalsIgnoreCase(key) && isCurrencyCode(currency) && Util.isDouble(value)) {
portfolio.setCash(currency, Double.parseDouble(value));
} else if ("BuyingPower".equalsIgnoreCase(key) && isCurrencyCode(currency) && Util.isDouble(value)) {
portfolio.setBaseCurrency(currency);
} else if ("ExchangeRate".equalsIgnoreCase(key) && isCurrencyCode(currency) && Util.isDouble(value)) {
portfolio.setExchangeRate(currency, Double.parseDouble(value));
}
} catch (Throwable t) {
logger.error(t.getMessage(), t);
}
}
@Override
public void updatePortfolio(Contract contract, int qty, double marketPrice, double marketValue, double averageCost, double unrealizedPNL, double realizedPNL,
String accountName) {
try {
if (logger.isDebugEnabled())
logger.debug("updatePortfolio: {} {} {} {} {} {} {} {}", new Object[] { Util.toString(contract), qty, marketPrice, marketValue, averageCost,
unrealizedPNL, realizedPNL, accountName });
if (qty != 0) {
Symbol symbol = toSymbol(contract);
double costBasis = averageCost / (contract.m_multiplier != null ? Integer.parseInt(contract.m_multiplier) : 1);
Position position = new Position(symbol, qty, costBasis, 0.0);
portfolio.setPosition(symbol, position);
logger.info("Updated {}, last: {}, unrealized pnl: {}", new Object[] { position, marketPrice, position.getProfitLoss(marketPrice) });
}
} catch (Throwable t) {
logger.error(t.getMessage(), t);
}
}
@Override
public void accountDownloadEnd(String accountName) {
try {
if (logger.isDebugEnabled())
logger.debug("accountDownloadEnd: {}", accountName);
logger.info("Updated {}", portfolio);
socket.reqAccountUpdates(false, ACCOUNT_CODE.get());
} catch (Throwable t) {
logger.error(t.getMessage(), t);
}
}
@Override
public void updateAccountTime(String timeStamp) {
try {
if (logger.isDebugEnabled())
logger.debug("updateAccountTime: {}", timeStamp);
} catch (Throwable t) {
logger.error(t.getMessage(), t);
}
}
@Override
public void error(int reqId, int errorCode, String errorMsg) {
try {
Object req = openOrdersById.get(reqId);
if (req == null) {
super.error(reqId, errorCode, errorMsg);
return;
}
OpenOrder openOrder = (OpenOrder) req;
String message = errorCode + ": " + errorMsg;
lastMessage = message;
switch (errorCode) {
case 161:
// 161: Cancel attempted when order is not in a cancellable
// state
logger.warn("Received error for {}: {}", openOrder, message);
break;
case 202:
// 202: Order Cancelled
break;
case 110:
// 110: price does not conform to the minimum price variation
// for this
// contract
default:
openOrder.setFailed();
openOrdersById.remove(reqId);
logger.warn("Received error for {}: {}", openOrder, message);
}
} catch (Throwable t) {
// Do not allow exceptions come back to the socket -- it will cause
// disconnects
logger.error(t.getMessage(), t);
}
}
private static boolean isCurrencyCode(String str) {
try {
return (Currency.getInstance(str.toUpperCase()) != null);
} catch (IllegalArgumentException e) {
}
return false;
}
@Override
public Position setPosition(Symbol symbol, int quantity) {
// TODO Auto-generated method stub
return null;
}
@Override
public Position setPosition(Symbol symbol, int quantity, ExecutionMethod executionMethod) {
// TODO Auto-generated method stub
return null;
}
@Override
public void cancelExecution(Symbol symbol) {
// TODO Auto-generated method stub
}
@Override
public void cancelExecutions() {
// TODO Auto-generated method stub
}
@Override
public Portfolio getPortfolio() {
return portfolio;
}
@Override
public PerformanceTracker getPerformanceTracker() {
return performanceTracker;
}
@Override
public Commission getCommission() {
return commission;
}
@Override
public void setCommission(Commission commission) {
this.commission = commission;
}
public static void main(String[] args) {
Trader trader = new IBTrader("localhost:4000", 22);
Configurable.configure("jtrade.trader.IBTrader#ACCOUNT_CODE", "DU66791");
try {
// Symbol s = SymbolFactory.getESFutureSymbol(new DateTime());
Symbol s = SymbolFactory.getSymbol("AAPL-SMART-USD-STOCK");
trader.connect();
Thread.sleep(2000);
try {
trader.placeOrder(s, 1, "testref1");
} catch (Exception e) {
e.printStackTrace();
}
Thread.sleep(2000);
try {
trader.placeOrder(s, -1, "testref2");
} catch (Exception e) {
e.printStackTrace();
}
Thread.sleep(2000);
trader.cancelOrder(s, null);
} catch (Exception e) {
e.printStackTrace();
} finally {
trader.disconnect();
}
}
}