// Copyright 2015 Ivan Popivanov // // 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 net.tradelib.core; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.logging.Logger; public class HistoricalReplay implements IBroker, IBarListener { private static final Logger logger = Logger.getLogger(HistoricalReplay.class.getName()); private HashMap<String,InstrumentCB> instrumentCBMap; private HistoricalDataFeed dataFeed = null; public HistoricalDataFeed getDataFeed() { return dataFeed; } public void setDataFeed(HistoricalDataFeed dataFeed) { this.dataFeed = dataFeed; this.dataFeed.addBarListener(this); } private Portfolio portfolio = null; private LocalDateTime lastBarTimestamp = null; // The bars belonging to this period (a day) private List<Bar> periodsBars = new ArrayList<Bar>(); // The order notifications private List<OrderNotification> orderNotifications = new ArrayList<OrderNotification>(); // The executions private List<Execution> executions = new ArrayList<Execution>(); protected IBrokerListener handler; public HistoricalReplay() { instrumentCBMap = new HashMap<String, InstrumentCB>(); portfolio = new Portfolio("default"); } public HistoricalReplay(Context context) { this(); assert context.historicalDataFeed != null; setDataFeed(context.historicalDataFeed); } public void start() throws Exception { if(dataFeed != null) dataFeed.start(); // Process the final set of bars processPeriodBars(); } public void subscribe(String symbol) throws Exception { if(dataFeed != null) dataFeed.subscribe(symbol); } public void unsubscribe(String symbol) throws Exception { if(dataFeed != null) dataFeed.unsubscribe(symbol); } public void submitOrder(Order order) throws Exception { getInstrumentCB(order.getSymbol()).newOrders.add(order); } public void reset() throws Exception { // Remove all per instrument runtime data instrumentCBMap.clear(); // Reset the data feed dataFeed.reset(); } public Portfolio getPortfolio(String name) { return portfolio; } @Override public Instrument getInstrument(String symbol) throws Exception { return dataFeed.getInstrument(symbol); } @Override public InstrumentVariation getInstrumentVariation(String provider, String symbol) throws Exception { return dataFeed.getInstrumentVariation(provider, symbol); } @Override public Position getPosition(Instrument instrument) throws Exception { return getInstrumentCB(instrument.getSymbol()).position; } private class InstrumentCB { // The instrument public Instrument instrument; // The position Position position; // The orders List<Order> orders; // The new orders. All orders are registered into this list first. Later // on they are moved to the "orders" list. List<Order> newOrders; InstrumentCB(Instrument i) { instrument = i; position = new Position(); orders = new ArrayList<Order>(); newOrders = new ArrayList<Order>(); } } private InstrumentCB getInstrumentCB(String symbol) throws Exception { InstrumentCB icb = instrumentCBMap.get(symbol); if(icb == null) { icb = new InstrumentCB(dataFeed.getInstrument(symbol)); instrumentCBMap.put(symbol, icb); } return icb; } private void addNewOrders(InstrumentCB icb) { icb.orders.addAll(icb.newOrders); icb.newOrders.clear(); } private void processOrders(InstrumentCB icb, Tick tick, boolean executeOnLimitOrStop) { // Scan all orders and check for a fill against the current tick. The execution // time must be different, hence we add a microsecond to the tick at each step. LocalDateTime ldt = tick.getDateTime(); for(Order order : icb.orders) { long previousPosition = icb.position.quantity; OrderFill of = order.tryFill(tick, previousPosition, executeOnLimitOrStop); if(of != null) { // We have an execution, bump up the timestamp ldt = ldt.plusNanos(1000); // Currently we only support single-entry and single-exit positions. if(previousPosition != 0 && of.getPosition() != 0) { if(previousPosition > of.getPosition()) { throw new UnsupportedOperationException( "Order partial position close at " + tick.getDateTime().format(DateTimeFormatter.ISO_INSTANT) + ": " + String.valueOf(previousPosition) + " -> " + String.valueOf(of.getPosition())); } else { throw new UnsupportedOperationException( "Order consecutive position opening at " + tick.getDateTime().format(DateTimeFormatter.ISO_INSTANT) + ": " + String.valueOf(previousPosition) + " -> " + String.valueOf(of.getPosition())); } } // Update the position icb.position = new Position(of.getPosition(), ldt); // Check whether we are closing a position. If so, any other exit order are cancelled. boolean removeExits = (previousPosition > 0 && of.getPosition() <= 0) || (previousPosition < 0 && of.getPosition() >= 0); // Cancel orders if necessary if(order.isOca()) { for(Order oo : icb.orders) { // Skip the current order if(oo == order) continue; // Cancel active, which are either exit or oca if(oo.isActive()) { if(oo.isExit() || oo.isOca() == order.isOca()) { oo.cancel(); } } } } else if (removeExits) { for(Order oo : icb.orders) { // Skip the current order if(oo == order) continue; // Cancel active, exit orders if(oo.isExit() && oo.isActive()) { oo.cancel(); } } } // logger_.info("appending transaction: " + icb.instrument.getSymbol() + // ": " + tick.getDateTime().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + // ": " + ldt.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + // ": " + Long.toString(previousPosition) + // ", " + Long.toString(of.getPosition()) + // ": " + Long.toString(of.getTransactionQuantity()) + // ", " + Long.toString(of.getFilledQuantity()) + // ": " + of.getFillPrice().toString()); // Mark the current order as filled order.fill(); // Add a transaction to the portfolio portfolio.addTransaction(icb.instrument, ldt, of.getTransactionQuantity(), of.getFillPrice(), 0.0); // Add an execution executions.add(new Execution(icb.instrument, ldt, of.getFillPrice(), of.getTransactionQuantity(), order.getSignal())); // Add a notification (posted after the order processing loop finishes) orderNotifications.add(new OrderNotification(order, executions.get(executions.size()-1))); } } } private void postOrderNotifications() throws Exception { for(OrderNotification on : orderNotifications) { if(handler != null) { handler.orderExecutedHandler(on); } } orderNotifications.clear(); } private void cleanupOrders(InstrumentCB icb, Bar bar) { // While improving performance by removing inactive orders // from the list, we lose the order history. Iterator<Order> it = icb.orders.iterator(); while(it.hasNext()) { Order order = it.next(); order.updateState(bar); if(!order.isActive()) it.remove(); } } @Override public void barNotification(Bar bar) throws Exception { assert lastBarTimestamp == null || bar.getDateTime().compareTo(lastBarTimestamp) >= 0 : "The feed must deliver bars in chronological order."; // Received a bar. If its timestamp is different than the // current period, we need to process all bars for the period. // Otherwise, the bar is simply added to the collection. if(lastBarTimestamp == null) { lastBarTimestamp = bar.getDateTime(); } else if(bar.getDateTime().compareTo(lastBarTimestamp) != 0) { processPeriodBars(); } periodsBars.add(bar); } // public void barNotification(Bar bar) throws Exception { // InstrumentCB icb = getInstrumentCB(bar.getSymbol()); // // // 1. All orders are eligible for execution at this point. // addNewOrders(icb); // // // 2. Process orders at open. At the open the limit and stop orders // // are executed on the tick (using false for executeOnLimitOrStop). // LocalDateTime ldt = LocalDateTime.of( // bar.getDateTime().getYear(), // bar.getDateTime().getMonthValue(), // bar.getDateTime().getDayOfMonth(), // 9, 0, 1); // processOrders(icb, new Tick(bar.getSymbol(), ldt, bar.getOpen()), false); // // // 3. Send notifications for the executed trades // postOrderNotifications(icb); // // // 4. Notify for the opening of the bar. We use a bar, not a Tick object, // // so that the callee can use (symbol, duration) to identify the bar set // // this bar belongs to. The callee may use only the open price from the bar. // Bar openBar = new Bar(bar); // openBar.setDateTime(ldt); // BigDecimal nan = BigDecimal.valueOf(Double.MIN_VALUE); // openBar.setHigh(nan); openBar.setLow(nan); openBar.setClose(nan); // openBar.setContractInterest(Long.MIN_VALUE); // openBar.setVolume(Long.MIN_VALUE); // openBar.setTotalInterest(Long.MIN_VALUE); // handler_.barOpenHandler(openBar); // // // 5. Pick up any new orders submitted during steps 3. and 4. // addNewOrders(icb); // // // 6. Process orders at high (assume at 11:00:01) // ldt = LocalDateTime.of( // bar.getDateTime().getYear(), // bar.getDateTime().getMonthValue(), // bar.getDateTime().getDayOfMonth(), // 11, 0, 1); // processOrders(icb, new Tick(bar.getSymbol(), ldt, bar.getHigh()), true); // // // No new orders are added here. Orders submitted during the *high* // // processing are not eligible for execution during the *low* processing. // // // 7. Send notifications for the executed trades // postOrderNotifications(icb); // // // 8. Process orders at low (assume at 13:00:01) // ldt = LocalDateTime.of( // bar.getDateTime().getYear(), // bar.getDateTime().getMonthValue(), // bar.getDateTime().getDayOfMonth(), // 13, 0, 1); // processOrders(icb, new Tick(bar.getSymbol(), ldt, bar.getLow()), true); // // // 9. Send notifications for the executed trades // postOrderNotifications(icb); // // // 10. Publish the bar, but it's not closed yet - this is to accommodate trading // // where the signal is computed at the close and the trading takes place at the close. // ldt = LocalDateTime.of( // bar.getDateTime().getYear(), // bar.getDateTime().getMonthValue(), // bar.getDateTime().getDayOfMonth(), // 15, 59, 59); // Bar closeBar = new Bar(bar); // closeBar.setDateTime(ldt); // handler_.barCloseHandler(closeBar); // // // 11. Pick up any new orders submitted during the previous two steps. Everything // // is eligible to be processed at the close. // addNewOrders(icb); // // // 12. Process orders at close // processOrders(icb, new Tick(bar.getSymbol(), ldt, bar.getClose()), false); // // // 13. Send notifications for the executed trades // postOrderNotifications(icb); // // // 14. The bar is closed // ldt = LocalDateTime.of( // bar.getDateTime().getYear(), // bar.getDateTime().getMonthValue(), // bar.getDateTime().getDayOfMonth(), // 16, 0, 0); // closeBar.setDateTime(ldt); // handler_.barClosedHandler(closeBar); // // // 15. Make all orders eligible // addNewOrders(icb); // // // 16. It's not safe to cleanup the order vectors earlier, since notifications // // point straight into the order vector. So all order updates (expiration and/or // // removal from the list) had to be postponed til now. // cleanupOrders(icb, closeBar); // } protected void processPeriodBars() throws Exception { // Process orders at the open for(Bar bar : periodsBars) { InstrumentCB icb = getInstrumentCB(bar.getSymbol()); // All orders are eligible for execution at this point. addNewOrders(icb); // Process orders at open. At the open the limit and stop orders // are executed on the tick (using false for executeOnLimitOrStop). LocalDateTime ldt = LocalDateTime.of( bar.getDateTime().getYear(), bar.getDateTime().getMonthValue(), bar.getDateTime().getDayOfMonth(), 9, 0, 1); processOrders(icb, new Tick(bar.getSymbol(), ldt, bar.getOpen()), false); } // Send notifications order notifications postOrderNotifications(); // Notify for the opening of the bar. We use a bar, not a Tick object, // so that the callee can use (symbol, duration) to identify the bar set // this bar belongs to. The callee may use only the open price from the bar. for(Bar bar : periodsBars) { LocalDateTime ldt = LocalDateTime.of( bar.getDateTime().getYear(), bar.getDateTime().getMonthValue(), bar.getDateTime().getDayOfMonth(), 9, 0, 1); Bar openBar = new Bar(bar); openBar.setDateTime(ldt); openBar.setHigh(Double.NaN); openBar.setLow(Double.NaN); openBar.setClose(Double.NaN); openBar.setContractInterest(Long.MIN_VALUE); openBar.setVolume(Long.MIN_VALUE); openBar.setTotalInterest(Long.MIN_VALUE); if(handler != null) { handler.barOpenHandler(openBar); } // Pick up any new orders submitted during the previous steps. addNewOrders(getInstrumentCB(bar.getSymbol())); } // Process orders at low (assume at 11:00:01) for(Bar bar : periodsBars) { LocalDateTime ldt = LocalDateTime.of( bar.getDateTime().getYear(), bar.getDateTime().getMonthValue(), bar.getDateTime().getDayOfMonth(), 11, 0, 1); processOrders(getInstrumentCB(bar.getSymbol()), new Tick(bar.getSymbol(), ldt, bar.getLow()), true); } // No new orders are added here. Orders submitted during the *high* // processing are not eligible for execution during the *low* processing. // Send notifications for the executed trades postOrderNotifications(); // Process orders at high (assume at 13:00:01) for(Bar bar : periodsBars) { LocalDateTime ldt = LocalDateTime.of( bar.getDateTime().getYear(), bar.getDateTime().getMonthValue(), bar.getDateTime().getDayOfMonth(), 13, 0, 1); processOrders(getInstrumentCB(bar.getSymbol()), new Tick(bar.getSymbol(), ldt, bar.getHigh()), true); } // Publish the bar, but it's not closed yet - this is to accommodate // trading where the signal is computed at the close and the trading // takes place at the close. for(Bar bar : periodsBars) { LocalDateTime ldt = LocalDateTime.of( bar.getDateTime().getYear(), bar.getDateTime().getMonthValue(), bar.getDateTime().getDayOfMonth(), 15, 59, 59); Bar closeBar = new Bar(bar); closeBar.setDateTime(ldt); if(handler != null) { handler.barCloseHandler(closeBar); } // Pick up any new orders submitted during the previous two steps. // Everything is eligible to be processed at the close. addNewOrders(getInstrumentCB(bar.getSymbol())); } for(Bar bar : periodsBars) { // Process orders at close LocalDateTime ldt = LocalDateTime.of( bar.getDateTime().getYear(), bar.getDateTime().getMonthValue(), bar.getDateTime().getDayOfMonth(), 15, 59, 59); processOrders(getInstrumentCB(bar.getSymbol()), new Tick(bar.getSymbol(), ldt, bar.getClose()), false); } // Send notifications for the executed trades postOrderNotifications(); // The bar is closed for(Bar bar : periodsBars) { LocalDateTime ldt = LocalDateTime.of( bar.getDateTime().getYear(), bar.getDateTime().getMonthValue(), bar.getDateTime().getDayOfMonth(), 16, 0, 0); Bar closeBar = new Bar(bar); closeBar.setDateTime(ldt); if(handler != null) { handler.barClosedHandler(closeBar); } // Make all orders eligible InstrumentCB icb = getInstrumentCB(bar.getSymbol()); addNewOrders(icb); // It's not safe to cleanup the order vectors earlier, since notifications // point straight into the order vector. So all order updates (expiration // and/or removal from the list) had to be postponed till now. cleanupOrders(icb, closeBar); } // Clear the bar list periodsBars.clear(); } @Override public void addBrokerListener(IBrokerListener listener) throws Exception { handler = listener; } @Override public void cancelAllOrders() throws Exception { for(InstrumentCB icb : instrumentCBMap.values()) { for(Order oo : icb.orders) { if(!oo.isCancelled()) oo.cancel(); } } } @Override public void cancelAllOrders(String symbol) throws Exception { InstrumentCB icb = getInstrumentCB(symbol); if(icb == null) return; for(Order oo : icb.orders) { if(!oo.isCancelled()) oo.cancel(); } } }