/******************************************************************************* * Copyright (c) 2013 Luigi Sgro. All rights reserved. This * program and the accompanying materials are made available under the terms of * the Eclipse Public License v1.0 which accompanies this distribution, and is * available at http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Luigi Sgro - initial API and implementation ******************************************************************************/ package com.quantcomponents.algo.service; import java.net.ConnectException; import java.util.Collection; import java.util.Date; import java.util.Deque; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.logging.Level; import java.util.logging.Logger; import com.quantcomponents.algo.ICommissionCalculator; import com.quantcomponents.algo.IOrder; import com.quantcomponents.algo.IOrderStatusListener; import com.quantcomponents.algo.IPosition; import com.quantcomponents.algo.IPositionListener; import com.quantcomponents.algo.ISimulatedExecutionService; import com.quantcomponents.algo.ITrade; import com.quantcomponents.algo.OrderBean; import com.quantcomponents.algo.PositionBean; import com.quantcomponents.algo.TradeBean; import com.quantcomponents.core.exceptions.RequestFailedException; import com.quantcomponents.core.model.IContract; import com.quantcomponents.core.model.ISeries; import com.quantcomponents.core.model.ISeriesListener; import com.quantcomponents.core.model.ISeriesPoint; import com.quantcomponents.core.model.OrderSide; import com.quantcomponents.core.model.OrderType; import com.quantcomponents.core.model.beans.ContractBean; import com.quantcomponents.marketdata.IStockDataCollection; public class SimulatedExecutionService implements ISimulatedExecutionService { private static final Logger logger = Logger.getLogger(SimulatedExecutionService.class.getName()); private final ConcurrentLinkedQueue<ITrade> trades = new ConcurrentLinkedQueue<ITrade>(); private final Map<IContract, SeriesListener> priceListeners = new HashMap<IContract, SeriesListener>(); private final Map<ISeries<Date, Double, ? extends ISeriesPoint<Date, Double>>, SeriesListener> listenersBySeries = new HashMap<ISeries<Date, Double, ? extends ISeriesPoint<Date, Double>>, SeriesListener>(); private final Set<IOrderStatusListener> orderStatusListeners = new HashSet<IOrderStatusListener>(); private final Set<IPositionListener> orderPositionListeners = new HashSet<IPositionListener>(); private final LinkedList<OrderInfo> currentOrders = new LinkedList<OrderInfo>(); private final LinkedList<OrderInfo> nextOrders = new LinkedList<OrderInfo>(); private final PositionCalculator positionCalculator = new PositionCalculator(); private final ICommissionCalculator commissionCalculator; private double slippage = 0.0; private double tradeCommission = 0.0; private double defaultBidAskSpread = 0.0; private int nextInternalId; private int nextOcaGroupId; private static class OrderInfo { public OrderInfo(IOrder order, int internalId, Integer ocaGroupId, Integer parentOrderId) { this.order = order; this.internalId = internalId; this.ocaGroupId = ocaGroupId; this.parentOrderId = parentOrderId; } public OrderInfo clone() { OrderInfo cloned = new OrderInfo(order, internalId, ocaGroupId, parentOrderId); cloned.toRemove = toRemove; return cloned; } int internalId; IOrder order; Integer ocaGroupId; Integer parentOrderId; boolean toRemove; } private class SeriesListener implements ISeriesListener<Date, Double> { private final IContract contract; private volatile Double lastPrice; private volatile Date lastPriceTimestamp; public SeriesListener(IContract contract) { this.contract = contract; } @Override public void onItemUpdated(ISeriesPoint<Date, Double> existingItem, ISeriesPoint<Date, Double> updatedItem) { double newPrice = updatedItem.getValue(); lastPriceTimestamp = updatedItem.getIndex(); if (lastPrice == null || newPrice != lastPrice) { lastPrice = newPrice; if (contract != null) { positionCalculator.onPriceUpdate(contract, updatedItem); } processOrders(); notifyPositionListeners(contract); } } @Override public void onItemAdded(ISeriesPoint<Date, Double> newItem) { onItemUpdated(null, newItem); } public Double getLastPrice() { return lastPrice; } public Date getLastPriceTimestamp() { return lastPriceTimestamp; } } /** * Backtest execution service * @param commissionCalculator An {@link ICommissionCalculator} * @param autoReset Set to true if the position must be reset when a stale price is received: useful when repeating tests * @throws ConnectException * @throws RequestFailedException */ public SimulatedExecutionService(ICommissionCalculator commissionCalculator) { this.commissionCalculator = commissionCalculator; } @Override public synchronized void addOrderStatusListener(IOrderStatusListener listener) { orderStatusListeners.add(listener); } @Override public synchronized void removeOrderStatusListener(IOrderStatusListener listener) { orderStatusListeners.remove(listener); } @Override public synchronized String sendOrder(IOrder order) { int internalId = nextInternalId(); OrderBean orderBean = OrderBean.copyOf(order); orderBean.setId(Integer.toString(internalId)); processOrder(new OrderInfo(orderBean, internalId, null, null)); removeDeletedOrders(); return Integer.toString(internalId); } @Override public synchronized String[] sendBracketOrders(IOrder parent, IOrder[] children) { String[] ids = new String[children.length + 1]; int parentInternalId = nextInternalId(); ids[0] = Integer.toString(parentInternalId); int ocaGroupId = nextOcaGroupId(); OrderBean parentOrderBean = OrderBean.copyOf(parent); parentOrderBean.setId(Integer.toString(parentInternalId)); currentOrders.add(new OrderInfo(parentOrderBean, parentInternalId, null, null)); for (int i = 0; i < children.length; i++) { IOrder child = children[i]; OrderBean childOrderBean = OrderBean.copyOf(child); int childId = nextInternalId(); childOrderBean.setId(Integer.toString(childId)); ids[i + 1] = Integer.toString(childId); currentOrders.add(new OrderInfo(childOrderBean, childId, ocaGroupId, parentInternalId)); } processOrders(); return ids; } protected synchronized double getSlippage() { return slippage; } protected synchronized void setSlippage(double slippage) { this.slippage = slippage; } protected synchronized double getTradeCommission() { return tradeCommission; } protected synchronized void setTradeCommission(double tradeCommission) { this.tradeCommission = tradeCommission; } public synchronized double getDefaultBidAskSpread() { return defaultBidAskSpread; } public synchronized void setDefaultBidAskSpread(double defaultBidAskSpread) { this.defaultBidAskSpread = defaultBidAskSpread; } @Override public synchronized void addPositionListener(IPositionListener listener) throws ConnectException { orderPositionListeners.add(listener); } @Override public synchronized void removePositionListener(IPositionListener listener) throws ConnectException { orderPositionListeners.remove(listener); } @Override public Deque<ITrade> getTrades() { return new LinkedList<ITrade>(trades); } private boolean processOrder(OrderInfo orderInfo) { if (!OrderType.LIMIT.equals(orderInfo.order.getType()) && !OrderType.MARKET.equals(orderInfo.order.getType()) && !OrderType.STOP.equals(orderInfo.order.getType())) { throw new UnsupportedOperationException("Only LIMIT, MARKET and STOP orders are supported"); } for (IOrderStatusListener listener : orderStatusListeners) { listener.onOrderSubmitted(Integer.toString(orderInfo.internalId), orderInfo.parentOrderId == null); } Double executionPrice = null; boolean executed = false; if (OrderSide.BUY.equals(orderInfo.order.getSide())) { executionPrice = getActualBuyPrice(orderInfo.order.getContract()); if (OrderType.MARKET.equals(orderInfo.order.getType()) || OrderType.LIMIT.equals(orderInfo.order.getType()) && executionPrice <= orderInfo.order.getLimitPrice() || OrderType.STOP.equals(orderInfo.order.getType()) && executionPrice >= orderInfo.order.getAuxPrice()) { executeTrade(orderInfo, orderInfo.order.getAmount(), executionPrice); executed = true; } } else if (OrderSide.SELL.equals(orderInfo.order.getSide())) { executionPrice = getActualSellPrice(orderInfo.order.getContract()); if (OrderType.MARKET.equals(orderInfo.order.getType()) || OrderType.LIMIT.equals(orderInfo.order.getType()) && executionPrice >= orderInfo.order.getLimitPrice() || OrderType.STOP.equals(orderInfo.order.getType()) && executionPrice <= orderInfo.order.getAuxPrice()) { executeTrade(orderInfo, orderInfo.order.getAmount(), executionPrice); executed = true; } } if (executed) { processChildren(orderInfo.internalId); if (orderInfo.ocaGroupId != null) { cancelOcaGroup(orderInfo.ocaGroupId); } } else { OrderInfo cloned = orderInfo.clone(); cloned.toRemove = false; nextOrders.add(cloned); // leave it in the next orders queue } return executed; } private void processOrders() { consolidateOrderQueue(); for (OrderInfo orderInfo : currentOrders) { if (!orderInfo.toRemove && orderInfo.parentOrderId == null) { orderInfo.toRemove = true; processOrder(orderInfo); } } removeDeletedOrders(); } private void processChildren(int parentId) { for (OrderInfo orderInfo : currentOrders) { if (!orderInfo.toRemove && orderInfo.parentOrderId != null && orderInfo.parentOrderId == parentId) { orderInfo.toRemove = true; orderInfo.parentOrderId = null; // next time don't wait for parent execution processOrder(orderInfo); } } } private void cancelOcaGroup(int ocaGroupId) { for (OrderInfo orderInfo : currentOrders) { if (!orderInfo.toRemove && orderInfo.ocaGroupId != null && orderInfo.ocaGroupId == ocaGroupId) { orderInfo.toRemove = true; } } for (OrderInfo orderInfo : nextOrders) { if (!orderInfo.toRemove && orderInfo.ocaGroupId != null && orderInfo.ocaGroupId == ocaGroupId) { orderInfo.toRemove = true; } } } private Double getLastPrice(IContract contract) { SeriesListener listener = priceListeners.get(contract); if (listener != null) { return listener.getLastPrice(); } else { logger.log(Level.WARNING, "Price listener not found for contract: " + contract); } return null; } private Date getLastPriceTimestamp(IContract contract) { SeriesListener listener = priceListeners.get(contract); if (listener != null) { return listener.getLastPriceTimestamp(); } else { logger.log(Level.WARNING, "Price listener not found for contract: " + contract); } return null; } private Double getActualBuyPrice(IContract contract) { Double lastPrice = getLastPrice(contract); if (lastPrice != null) { return lastPrice + defaultBidAskSpread / 2.0 + slippage; } else { return null; } } private Double getActualSellPrice(IContract contract) { Double lastPrice = getLastPrice(contract); if (lastPrice != null) { return lastPrice - defaultBidAskSpread / 2.0 - slippage; } else { return null; } } private int nextOcaGroupId() { return nextOcaGroupId++; } private int nextInternalId() { return nextInternalId++; } private void consolidateOrderQueue() { currentOrders.addAll(nextOrders); nextOrders.clear(); } private void removeDeletedOrders() { Iterator<OrderInfo> iteratorCurrent = currentOrders.iterator(); while (iteratorCurrent.hasNext()) { OrderInfo orderInfo = iteratorCurrent.next(); if (orderInfo.toRemove) { iteratorCurrent.remove(); for (IOrderStatusListener listener : orderStatusListeners) { listener.onOrderCancelled(Integer.toString(orderInfo.internalId)); } } } Iterator<OrderInfo> iteratorNext = nextOrders.iterator(); while (iteratorNext.hasNext()) { OrderInfo orderInfo = iteratorNext.next(); if (orderInfo.toRemove) { iteratorNext.remove(); for (IOrderStatusListener listener : orderStatusListeners) { listener.onOrderCancelled(Integer.toString(orderInfo.internalId)); } } } } private void executeTrade(OrderInfo orderInfo, int tradeAmount, double executionPrice) { double multiplier = 1.0; IContract contract = orderInfo.order.getContract(); if (contract.getMultiplier() != null && contract.getMultiplier() != 0) { multiplier = contract.getMultiplier(); } int signedTradeAmount = orderInfo.order.getSide() == OrderSide.BUY ? tradeAmount : -tradeAmount; double tradeCommissionAmount = commissionCalculator.calculateCommission(orderInfo.order, tradeAmount, executionPrice); double tradeCashFlow = -(signedTradeAmount * multiplier * executionPrice) - tradeCommissionAmount; double tradeAveragePrice = Math.abs(tradeCashFlow / multiplier / tradeAmount); TradeBean trade = new TradeBean(orderInfo.order, "Simulated", getLastPriceTimestamp(contract), tradeAmount, executionPrice, tradeAveragePrice); trades.add(trade); positionCalculator.onTrade(trade); for (IOrderStatusListener listener : orderStatusListeners) { listener.onOrderFilled(Integer.toString(orderInfo.internalId), tradeAmount, orderInfo.order.getAmount() == tradeAmount, executionPrice); } notifyPositionListeners(contract); notifyPositionListeners(ContractBean.cash(contract.getCurrency())); } private void notifyPositionListeners(IContract contract) { IPosition position = positionCalculator.getPositions().get(contract); if (position != null) { for (IPositionListener listener : orderPositionListeners) { listener.onPositionUpdate(contract, new PositionBean(position)); } } } public synchronized void stop() { for (Map.Entry<ISeries<Date, Double, ? extends ISeriesPoint<Date, Double>>, SeriesListener> entry : listenersBySeries.entrySet()) { entry.getKey().removeSeriesListener(entry.getValue()); } } @Override public synchronized void setInputSeries(Collection<ISeries<Date, Double, ? extends ISeriesPoint<Date, Double>>> inputSeries) { for (ISeries<Date, Double, ? extends ISeriesPoint<Date, Double>> input : inputSeries) { IContract contract = null; if (input instanceof IStockDataCollection) { contract = ((IStockDataCollection) input).getContract(); } SeriesListener seriesListener = new SeriesListener(contract); input.addSeriesListener(seriesListener); if (contract != null) { priceListeners.put(contract, seriesListener); } listenersBySeries.put(input, seriesListener); } } }