package jforex.orders.merge; import java.math.BigDecimal; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; import com.dukascopy.api.*; import com.dukascopy.api.IEngine.OrderCommand; import com.dukascopy.api.util.DateUtils; /** * The strategy demonstrates how one can go about automatically merging positions * with SL/TP values without needing to use the IOrder.waitForUpdate. * * * The strategy on its start creates 4 orders which have SL or TP prices. * Since orders with SL or TP can't be merged, both price conditions have to be removed and after * successful merge put back to the merged order. * */ public class MergeRemoveSlTpOnMessage_ implements IStrategy { private IConsole console; private IEngine engine; private IHistory history; private Map> slMap = new HashMap>(); private Map> tpMap = new HashMap>(); private Map> mergedFrom = new HashMap>(); private Set tpAddInProgress = new HashSet(); private Set slAddInProgress = new HashSet(); private Set mergedWithCompletedSlTpAdd = new HashSet(); private Map slChangeTimes = new HashMap(); private Map tpChangeTimes = new HashMap(); private int mergeCount; @Override public void onStart(IContext context) throws JFException { engine = context.getEngine(); console = context.getConsole(); history = context.getHistory(); console.getOut().println("Start"); Instrument instrument = Instrument.EURUSD; double price = history.getLastTick(instrument).getBid(); // 0.01 BUY market order with SL and TP of 10 pips engine.submitOrder("order1", instrument, OrderCommand.BUY, 0.01, 0, 20, price - 0.0010, price + 0.0010); // 0.02 SELL market order without SL and TP engine.submitOrder("order2", instrument, OrderCommand.SELL, 0.02, 0, 20, 0, 0); // 0.02 BUY market order with SL of 10 pips engine.submitOrder("order3", instrument, OrderCommand.BUY, 0.02, 0, 20, price - 0.0010, 0); engine.submitOrder("order4", instrument, OrderCommand.BUY, 0.02, 0, 20, price - 0.0010, 0); } @Override public void onBar(Instrument instrument, Period period, IBar askBar, IBar bidBar) throws JFException {} @Override public void onMessage(IMessage message) throws JFException { IOrder order = message.getOrder(); if (order == null || message.getType() == IMessage.Type.NOTIFICATION) { return; } console.getOut().format("%s - %s %s SL=%.5f TP=%.5f", DateUtils.format(message.getCreationTime()), message, message.getReasons(), order.getStopLossPrice(), order.getTakeProfitPrice()).println(); Instrument instrument = order.getInstrument(); int filledPositionCount = 0; for(IOrder o : engine.getOrders(instrument)){ if(o.getState() == IOrder.State.FILLED){ filledPositionCount++; } } try { // check if the SL/TP has been put back mergeSlTpPutBackChecks(message); // remove SL/TP for "eligible" positions - just filled ones and the merged ones with just having been put back their SL/TP's if (filledPositionCount > 1 && (isJustFilledWithSlTp(message) || isMergedWithJustCompletedSlTpAdd(message))) { removeSlTpPrices(order); if (isMergedWithJustCompletedSlTpAdd(message)) { mergedWithCompletedSlTpAdd.remove(order); } return; // put back SL/TP to just merged positions } else if (message.getType() == IMessage.Type.ORDERS_MERGE_OK && mergedFrom.containsKey(order.getLabel())) { putBackSlTpPrices(order); //we can merge this position - if the other order has no SL/TP - we will merge them; //otherwise - we will remove the other position's SL/TP and merge in the next iteration } else if (filledWithoutSlTp(order) && filledPositionCount > 1) { Set inMergeProgress = new HashSet(); for (Set from : mergedFrom.values()) { inMergeProgress.addAll(from); } for (IOrder o2 : engine.getOrders(instrument)) { if (order != o2 && !inMergeProgress.contains(o2)) { // merge filled positions with no SL/TP if (filledWithoutSlTp(o2)) { IOrder merged = engine.mergeOrders("merged" + ++mergeCount, o2, order); mergedFrom.put(merged.getLabel(), new HashSet(Arrays.asList(order, o2))); break; //this would normally be a position with SL/TP which for a while has been the only position } else if (isFilledWithSlTp(o2)) { removeSlTpPrices(o2); break; } } } } } catch (Exception e) { console.getErr().println(order + " " + e); e.printStackTrace(); } } private boolean isMergedWithJustCompletedSlTpAdd(IMessage message){ return mergedWithCompletedSlTpAdd.contains(message.getOrder()) && message.getType() == IMessage.Type.ORDER_CHANGED_OK && isFilledWithSlTp(message.getOrder()); } private boolean isJustFilledWithSlTp(IMessage message){ return message.getType() == IMessage.Type.ORDER_FILL_OK && ( message.getOrder().getStopLossPrice() > 0 || message.getOrder().getTakeProfitPrice() > 0); } private boolean isFilledWithSlTp(IOrder order){ return order.getState() == IOrder.State.FILLED && (order.getStopLossPrice() > 0 || order.getTakeProfitPrice() > 0); } private boolean filledWithoutSlTp(IOrder order){ return order.getState() == IOrder.State.FILLED && order.getStopLossPrice() == 0 && order.getTakeProfitPrice() == 0; } private void mergeSlTpPutBackChecks(IMessage message){ mergeSlTpUpdateCheck(message, true); mergeSlTpUpdateCheck(message, false); } private void mergeSlTpUpdateCheck(IMessage message, boolean isSl){ IOrder order = message.getOrder(); Set priceAddInProgress = isSl ? slAddInProgress : tpAddInProgress; Set oppositePriceAddInProgress = isSl ? tpAddInProgress : slAddInProgress; IMessage.Reason reason = isSl ? IMessage.Reason.ORDER_CHANGED_SL : IMessage.Reason.ORDER_CHANGED_TP; if (message.getReasons().contains(reason) && priceAddInProgress.contains(order)) { priceAddInProgress.remove(order); if (!oppositePriceAddInProgress.contains(order)) { mergedWithCompletedSlTpAdd.add(order); } } } private void putBackSlTpPrices(IOrder order) throws JFException{ putBackPrice(order, true); putBackPrice(order, false); mergedFrom.remove(order.getLabel()); } // simply take the average of pip distances of the two merged-from orders private void putBackPrice(IOrder order, boolean isSl) throws JFException{ Instrument instrument = order.getInstrument(); Iterator it = mergedFrom.get(order.getLabel()).iterator(); IOrder o1 = it.next(); IOrder o2 = it.next(); Map> priceMap = isSl ? slMap : tpMap; Set priceAddInProgress = isSl ? slAddInProgress : tpAddInProgress; Double tp1 = priceMap.get(instrument).remove(o1); Double tp2 = priceMap.get(instrument).remove(o2); double tpNew = 0; if (anyGtZero(tp1, tp2)) { double tp1Pips = nullZero(tp1) ? 0 : Math.abs(o1.getOpenPrice() - tp1) / instrument.getPipValue(); double tp2Pips = nullZero(tp2) ? 0 : Math.abs(o2.getOpenPrice() - tp2) / instrument.getPipValue(); tpNew = order.getOpenPrice() + (tp1Pips + tp2Pips) / 2 * instrument.getPipValue() * (order.isLong() ^ isSl ? +1 : -1); setPrice(order, roundToPippette(tpNew, instrument), isSl); priceAddInProgress.add(order); } console.getNotif() .format("%6$s[%1$s]=%3$s %6$s[%2$s]=%4$s => %6$s[%7$s]=%5$.5f", o1.getLabel(), o2.getLabel(), tp1, tp2, tpNew, isSl? "SL" : "TP", order.getLabel()).println(); } private void removeSlTpPrices(IOrder order) throws JFException{ setPrice(order, 0, true); setPrice(order, 0, false); } private void setPrice(IOrder order, double price, boolean isSl) throws JFException{ double prevPrice = isSl ? order.getStopLossPrice() : order.getTakeProfitPrice(); if( Double.compare(price, prevPrice)==0){ return; } long time = engine.getType() == IEngine.Type.TEST ? history.getLastTick(order.getInstrument()).getTime() : System.currentTimeMillis(); Instrument instrument = order.getInstrument(); Map> priceMap = isSl ? slMap : tpMap; if (priceMap.get(instrument) == null) { priceMap.put(instrument, new HashMap()); } priceMap.get(instrument).put(order, prevPrice); Map changeTimes = isSl ? slChangeTimes : tpChangeTimes; if (changeTimes.get(order) != null && time - changeTimes.get(order) < 1000) { // make sure there is at least 1 second between the price change times // if the position meanwhile got closed - we don't need to wait anymore order.waitForUpdate(1001 - time + changeTimes.get(order), IOrder.State.CLOSED); } if(order.getState() != IOrder.State.FILLED){ return; } if(isSl){ order.setStopLossPrice(price); } else { order.setTakeProfitPrice(price); } changeTimes.put(order, System.currentTimeMillis()); } private boolean nullZero(Double d) { if (d == null || Double.compare(d, 0) == 0) { return true; } return false; } private boolean anyGtZero(Double...values){ for(Double d : values){ if(d != null && d>0){ return true; } } return false; } private static double roundToPippette(double amount, Instrument instrument) { return round(amount, instrument.getPipScale() + 1); } private static double round(double amount, int decimalPlaces) { return (new BigDecimal(amount)).setScale(decimalPlaces, BigDecimal.ROUND_HALF_UP).doubleValue(); } @Override public void onAccount(IAccount account) throws JFException { } @Override public void onStop() throws JFException { for (IOrder o : engine.getOrders()){ if(o.getState() == IOrder.State.FILLED || o.getState() == IOrder.State.OPENED){ o.close(); } } } @Override public void onTick(Instrument instrument, ITick tick) throws JFException { } }