JForex Example: Not letting profit turn to losses with a breakeven stop

While the dual-timeframe trading setup and much of the algorithm in my contest strategy are for competition only (read: not for use with real money) as I've warned numerous times, some of the risk management techniques used there are really what I use in real life. This being one of them. There's a saying in trading that "never let profits turns into losses." That is exactly the motivation behind this JForex code snippet. The following source code are excerpts from my Dukascopy JForex contest's July strategy. The complete source code file is available in that link. Back to this concept of not letting profits turn to losses. This is a matter of balancing between giving room for your trade to reach its potential and limiting your drawdown. If you're too careful, you may find yourself being whipsawed out before a price move can materialize. If you keep your leash too loose though, well, you may watch your profits turn into losses. The way I intrepret the saying is that once your trade is profitable enough, you shouldn't let it slip back into a loss. I implement the above statement in my automated strategy as follows. Note that everything goes in the onTick() method so that it watches your position on every tick. The line numbers correspond to the complete source code of my automated strategy. [java firstline="177"] boolean isLong; double open, stop, diff, newStop; for (IOrder order : engine.getOrders(instrument)) { if (order.getState() == IOrder.State.FILLED) { [/java] [java firstline="191"] isLong = order.isLong(); open = order.getOpenPrice(); stop = order.getStopLossPrice(); diff = (open - stop); // ********* BREAKEVEN *********************** if (isLong && diff > 0 && tick.getBid() > (open + diff)) { order.close(roundLot(order.getAmount() * beRatio)); // close a portion newStop = open + instrument.getPipValue() * LOCKPIP; order.setStopLossPrice(newStop); print(order.getLabel() + ": Moved STOP to breakeven"); } else if (!isLong && diff \< 0 && tick.getAsk() \< (open + diff)) { order.close(roundLot(order.getAmount() * beRatio)); newStop = open - (instrument.getPipValue() * LOCKPIP); order.setStopLossPrice(newStop); print(order.getLabel() + ": Moved STOP to breakeven"); } [/java] What this does is that it will partially exit a position and move the stop to breakeven once the price is equidistantly positive from your initial stop loss. For example, if my stop loss is 100 pips, then once the position is 100 pips in profit, it will exit partially and set the new stop to breakeven. Update: This is now integrated into the JFUtil open source project.

JForex Example: Dual-Timeframe Moving Averages Setup

This post describes the setup for my Dukascopy JForex automated trading strategy in July. Note that this strategy is built for competing in a contest and not for real trading (i.e., its purely a no cost gamble). Here is the step by step process of the setup for a long position:

  1. Determine the current trend by the relative position of current price to its moving average in a higher time frame. In particular, a price above the moving average in the higher time frame is deemed as bullish.
  2. Entry: Price moves above a moving average in the current time frame (red line in Figure 1) with a higher high, low, and close (HLC). As illustrated in Figure 1, the two bars circled.
  3. Exit: Crossing below a regular moving average (blue dotted line).

EURUSD setup

As you can see, the setup itself is simple. Less than a fifth of the source code is devoted to the entry and exit. Most of this strategy involves automated position sizing and risk management to not let profits turn into losses.

The complete source code of this strategy is available in the introductory post.

JForex Example: Multiple-time frame strategy

The key to my July JForex strategy is the use of multiple time frames. As Dr. Alexander Elder demonstrates in his famous book, Trading For A Living, a proper technical analysis should at least consider time frames five-times faster and slower than the one in which you trade. For example, if you trade on a 30 minute chart, the one faster is 30/5 = 6 minutes, which can be rounded as the five-minute chart. And the one slower would be 30 * 5 = 150 minutes. The closest standard time frame of which is a 4-hour chart.

For my July JForex strategy, it trades on the 30 minute chart and monitor the 4-hour chart in addition to the 30-minute chart. The strategy doesn't make use of a faster time frame for simplicity. This is surprisingly easy to do in Dukascopy's JForex API.

Referring to lines 81 to 87 of my strategy source code.

if (period == PERIODINT) {
    setSentiment(instrument, bidBar, askBar); 
    return;
} 
if (period != PERIODSHR) return; // skip all other periods

This chunk of code appears in the onBar() method. Since onBar() is called at the beginning of every price bar across all possible standard periods (from tick to weekly), it's just a matter of filtering out the redundant periods and/or catching the method when the period is correct. I have done both in the sample code. Line 81 is to catch the longer period, PERIODINT = Period.FOUR\_HOURS (line 54, not shown), in an if statement block. It calls my function setSentiment() and exits onBar() with the return, skipping everything in onBar() afterward. Line 87 is to skip all other periods that I'm not using. I could have used if (period == PERIODSHR) and put everything in an if block as in lines 81-85. But since the strategy is doing a lot more work in PERIODSHR, there would be a lot of codes in that IF block. So I am doing the reverse for the same result but simpler-looking codes. With this cleared, I can go over my dual moving average entry signal setup in the next post later this week.

Sixth place finish in the Dukascopy JForex July strategy contest

Update Dec 7, 2010: I have won a few more times in this contest including a first place and a third place. Dukascopy flew me to Geneva in September for an interview.

Preliminary results from the Dukascopy JForex Strategy Contest last month shows that I finished with a sixth place. That is a USD $1,000 win for me. This is the second time that I have won since April. As usual, I agreed to disclose my strategy so that I can receive 100% of the prize money. You will find the source code at the bottom of this post. I am very grateful of Dukascopy for providing this opportunity. This contest gave me with an incentive to learn their trading API as I have intended anyway. I have a live and funded trading account at Dukascopy that has yet to be touched.

Anyway, I will explain my strategy just like last time. This one is an improvement of my Keltner Channel and candlestick setup as I have discussed previously. Last time I had 200 lines of code. This time it's almost 500 lines. Most of the new codes are for position management. The basic setup is similar conceptually. Buy on retracement in an uptrend, and vice versa to short. Seeing that my strategy is quite long, I doubt I can explain it fully in a single post. As such, I will split this task into a series of future posts to be posted this month of August. (Update: The first post of the series is posted discussing the use of multiple time frame in JForex.) In the mean time, here is the complete source code for my strategy. Or, you can download the source file here directly via Dropbox.

/*
quantisan1 - JForex contest strategy
version 2.14
July 2010
Copyright 2010 Paul at Quantisan.com

Changelog:
v2.14   - commit for July 2010 competition
        - optimized parameters
        - fixed sentiment on start
        - added regression position sizing
v2.13   - name changed from ma_retrace2 to quantisan1
        - new rules in July
        - removed all @Configurable
        - ensure max. 1 position in acct
        - added position sizing
        - breakeven stop
        - two-tier time frame strategy
v2.12   - commit for June 2010 competition
v2.11   - fixed getLabel open position number overlap bug
v2.10   - removed @Config modifying open position to adhere to contest rules
        - drawTrade() NP exception bug
        - added exit automation
        - added exit target and stop limit prices
v2.0    - renamed from ma_scalp1 (April 2010 contest)

*/
package jforex;

import com.dukascopy.api.*;
import java.util.*;
import java.awt.Color;

public class quantisan1 implements IStrategy {
    private IEngine engine;
    private IIndicators indicators;
    private IHistory history;
    private int tagCounter;
    private IConsole console;
    private IContext context;
    private double acctEquity;
    private double[] maShr1 = new double[Instrument.values().length];
    private double[] maInt1 = new double[Instrument.values().length];
    private double[] atr = new double[Instrument.values().length];

    private Sentiment[] sentiment = new Sentiment[Instrument.values().length];      
    private boolean tradingAllowed;

    // parameters
    private static final int LOCKPIP = 3;
    private static final int SLIPPAGE = 5;

    private static final Period PERIODSHR = Period.THIRTY_MINS;
    private static final Period PERIODINT = Period.FOUR_HOURS;

    private static final Filter indFilter = Filter.ALL_FLATS;
    private static final IIndicators.MaType maType = IIndicators.MaType.SMA;

    // position management
    private static final double startEquity = 100000d;

    private double beRatio = 0.66;

    private double riskPct = 20.0;
    private static final double maxRiskPct = 30.0;
    private static final double minRiskPct = 1;
    private static final double riskAdjStep = 1.2;

    // indicators
    private static final double atrFactor = 7.0;
    private int lenShr = 13;    
    private int lenInt = 55;

    // ** onBar used for entries **
    public void onBar(Instrument instrument, Period period, IBar askBar, IBar bidBar) throws JFException 
    {
        double maShr, maInt;
        double stopLoss, lot, chanTop, chanBot;

        // higher time frame get sentiment
        if (period == PERIODINT)
        {               
            setSentiment(instrument, bidBar, askBar);           
            return;         
        }

        if (period != PERIODSHR)    return;     // skip all other periods

        this.atr[instrument.ordinal()] = indicators.atr(instrument, PERIODSHR, OfferSide.BID, lenShr, 
            indFilter, 1, bidBar.getTime(), 0)[0];

        // moving averages
        maShr = indicators.ma(instrument, period,
            OfferSide.BID, IIndicators.AppliedPrice.MEDIAN_PRICE, 
            lenShr, maType, indFilter, 1, bidBar.getTime(), 0)[0];

        maInt = indicators.ma(instrument, period,
            OfferSide.BID, IIndicators.AppliedPrice.MEDIAN_PRICE, 
            lenInt, maType, indFilter, 1, bidBar.getTime(), 0)[0];

        chanTop = indicators.ma(instrument, period,
            OfferSide.ASK, IIndicators.AppliedPrice.HIGH, 
            lenInt, maType, indFilter, 1, bidBar.getTime(), 0)[0];

        chanBot = indicators.ma(instrument, period,
            OfferSide.BID, IIndicators.AppliedPrice.LOW, 
            lenInt, maType, indFilter, 1, bidBar.getTime(), 0)[0];

        // if not enough bars
        if (this.maShr1[instrument.ordinal()] <= 0 || maShr <= 0 ||
            this.maInt1[instrument.ordinal()] <= 0 || maInt <= 0 || 
            this.atr[instrument.ordinal()] <= 0) 
        {
         updateMA(instrument, maShr, maInt);
            return;
        }

        IBar prevBidBar = history.getBar(instrument, period, OfferSide.BID, 2);
        IBar prevAskBar = history.getBar(instrument, period, OfferSide.ASK, 2);     
        if (prevBidBar == null || prevAskBar == null)   return;     //not formed yet

// *****   ENTRY SETUP   *******************************************************
        if (tradingAllowed)
        {                                   
            if (this.sentiment[instrument.ordinal()] == Sentiment.BULL && 
                bidBar.getLow() > chanBot && prevBidBar.getLow() < chanBot && 
                bidBar.getLow() > prevBidBar.getLow() && bidBar.getHigh() > prevBidBar.getHigh() &&
                bidBar.getClose() > prevBidBar.getClose())
            {
                // go long
                stopLoss = bidBar.getLow() - (this.atr[instrument.ordinal()] * atrFactor);
                stopLoss = roundPip(stopLoss);

                String label = getLabel(instrument);
                lot = getLot(instrument, bidBar.getClose());

                engine.submitOrder(label, instrument, IEngine.OrderCommand.BUY, 
                    lot, 0, SLIPPAGE, stopLoss, 0);         
                print(label + ": Long entry, lot = " + lot);            
            }
            else if (this.sentiment[instrument.ordinal()] == Sentiment.BEAR &&
                askBar.getHigh() < chanTop && prevAskBar.getHigh() > chanTop &&                 
                askBar.getLow() < prevAskBar.getLow() && askBar.getHigh() < prevAskBar.getHigh() &&
                askBar.getClose() < prevAskBar.getClose())
            {
                // go short
                stopLoss = askBar.getHigh() + (this.atr[instrument.ordinal()] * atrFactor);
                stopLoss = roundPip(stopLoss);

                String label = getLabel(instrument);
                lot = getLot(instrument, askBar.getClose());

                engine.submitOrder(label, instrument, IEngine.OrderCommand.SELL, 
                    lot, 0, SLIPPAGE, stopLoss, 0);         
                print(label + ": Short entry, lot = " + lot);
            }
        }



    // ************ onBar clean up ************


        updateMA(instrument, maShr, maInt);
    }

// ****************************************************************************


    // ** onTICK: for open position management **
    public void onTick(Instrument instrument, ITick tick) throws JFException {


        if (maInt1[instrument.ordinal()] <= 0)          // ma not ready
            return;

        boolean isLong; 
        double open, stop, diff, newStop;

        for (IOrder order : engine.getOrders(instrument)) {
            if (order.getState() == IOrder.State.FILLED) {

                // Rule 6.3, profit per trade cannot be > 50%
                if (order.getProfitLossInUSD()> this.acctEquity*0.49) {
               order.close();
               print(order.getLabel() + ": CLOSED for Rule 6.3");
            }

                if (order.getProfitLossInAccountCurrency() <= 0d) continue; // skip if losing (use stop)

                isLong = order.isLong();
                open = order.getOpenPrice();
                stop = order.getStopLossPrice(); 
                diff = (open - stop);

// ********* BREAKEVEN *********************************************************                        
                if (isLong && diff > 0 && tick.getBid() > (open + diff))
                {
                    order.close(roundLot(order.getAmount() * beRatio)); // close a portion


                    newStop = open + instrument.getPipValue() * LOCKPIP;
                    order.setStopLossPrice(newStop);                        
               print(order.getLabel() + ": Moved STOP to breakeven");
                }
                else if (!isLong && diff < 0 && tick.getAsk() < (open + diff))
                {
                    order.close(roundLot(order.getAmount() * beRatio));

                    newStop = open - (instrument.getPipValue() * LOCKPIP);
                    order.setStopLossPrice(newStop);
                    print(order.getLabel() + ": Moved STOP to breakeven");
                }

// ********* TAKE PROFIT *******************************************************                
                // get out if price crossed maInt1
                if (isLong ? tick.getAsk() < maInt1[instrument.ordinal()] : 
                    tick.getBid() > maInt1[instrument.ordinal()]) 
                {                       
                    order.close();                          
                    print(order.getLabel() + ": Take PROFIT");
                }
            }
        }
// *****************************************************************************
    }


// *****  UTILS start  *********************************************************


    private double getLot(Instrument instrument, double price) throws JFException
    {
        double riskAmt;
        double lotSize = 0;

        riskAmt = this.acctEquity * (this.riskPct / 100d); // CCYUSD only
        lotSize = riskAmt / (this.atr[instrument.ordinal()] * this.atrFactor);

        lotSize /= 1000000d;                    // in millions
        return roundLot(lotSize);
    }

    private double roundLot(double lot)
    {   
        lot = (int)(lot * 1000) / (1000d);      // 1000 units min.
        return lot;
    }

    // keep prev MA instead of calc to improve performance
    private void updateMA(Instrument instrument, double maShr, double maInt) throws JFException
    {
        this.maShr1[instrument.ordinal()] = maShr;
        this.maInt1[instrument.ordinal()] = maInt;
    }

    private double roundPip(double value) {
    // rounding to nearest half, 0, 0.5, or 1
        int pipsMultiplier = value <= 20 ? 10000 : 100;
        int rounded = (int) (value * pipsMultiplier * 10 + 0.5);
        rounded *= 2;
        rounded = (int) (((double) rounded) / 10d + 0.5d);
        value = ((double) rounded) / 2d;
        value /= pipsMultiplier;
        return value;
    }

    private String getLabel(Instrument instrument) throws JFException {
        int max = 0;
        int parse;

        // check for open position label numbers
        for (IOrder order : engine.getOrders(instrument)) {
            if (order.getState() == IOrder.State.FILLED) {
                parse = 0;
                try {
                    parse = Integer.parseInt(order.getLabel().substring(5));
                }
                catch (NullPointerException e) {
                    print(e.getMessage() + ": getLabel()");
                    parse = 0;
                }

                if (parse > max) max = parse;
            }
        }
        if (max > tagCounter)       // continue count of existing number
            tagCounter = max;

        String label = instrument.name();
        label = label.substring(0, 3) + label.substring(3, 6);
        label = label + (tagCounter++);
        label = label.toLowerCase();
        return label;
   }

    public void print(String string) {
        this.console.getOut().println(string);
    }

    private void drawTrade(IOrder myOrder) {
        Instrument instrument;
        String label;
        boolean isLong;
        double openPrice, closePrice;
        IChart myChart;
        IChart.Type objType;
        IChartObject myObject;
        Color color;

        instrument = myOrder.getInstrument();
        isLong = myOrder.isLong();
        label = myOrder.getLabel();

        myChart = this.context.getChart(instrument);
        if (myChart == null)    return;     // no chart

        if (myOrder.getState() == IOrder.State.FILLED) 
        {
            objType = IChart.Type.PRICEMARKER;
            try {
                myObject = myChart.draw(label, objType, 
                    myOrder.getFillTime(), myOrder.getOpenPrice());             
            } catch (NullPointerException e) {
                print(e.getMessage() + ": chart not available");
                return;
            }

            color = isLong ? Color.CYAN : Color.PINK;

            myObject.setColor(color);
        }
        else if (myOrder.getState() == IOrder.State.CLOSED) 
        {

            try {
                myChart.remove(label);          // remove entry marker
            } catch (NullPointerException e) {
                print(e.getMessage() + ": chart not available");
                return;
            }

            objType = IChart.Type.SHORT_LINE;
            openPrice = myOrder.getOpenPrice();
            closePrice = myOrder.getClosePrice();
            try {
                myObject = myChart.draw(label, objType, 
                    myOrder.getFillTime(), openPrice,
                    myOrder.getCloseTime(), closePrice);            
            } catch (NullPointerException e) {
                print(e.getMessage() + ": chart not available");
                return;
            }

            if (isLong) {
                color = openPrice < closePrice ? Color.BLUE : Color.RED;
            }
            else {
                color = openPrice > closePrice ? Color.BLUE : Color.RED;
            }

            myObject.setColor(color);
            //myObject.setAttrInt(IChartObject.ATTR_INT.WIDTH, 1);
        }


    }



    public void adjRiskPct(IOrder order) throws JFException
    {
            double riskAdj, x;

            x = this.acctEquity;
            // regression
            this.riskPct =  -2.88462e-10 * x * x  - 0.000156731 * x + 38.5577;

            riskAdj = this.riskAdjStep;
            riskAdj *= order.getProfitLossInAccountCurrency() > 0.01 * this.acctEquity ? 1.0 : -2.0;
            this.riskPct += riskAdj;

            // ensure riskPct is within defined range
            if (this.riskPct > this.maxRiskPct) this.riskPct = this.maxRiskPct;
            else if (this.riskPct < this.minRiskPct)    this.riskPct = this.minRiskPct;

            print("Adjusted riskPct to: " + this.riskPct);
    }

    private void setSentiment(Instrument instrument, IBar bidBar, IBar askBar) throws JFException
    {
        double chanTop, chanBot;
        chanTop = this.indicators.ma(instrument, PERIODINT,
            OfferSide.ASK, IIndicators.AppliedPrice.HIGH, 
            lenInt, maType, indFilter, 1, askBar.getTime(), 0)[0];

        chanBot = this.indicators.ma(instrument, PERIODINT,
            OfferSide.BID, IIndicators.AppliedPrice.LOW, 
            lenInt, maType, indFilter, 1, bidBar.getTime(), 0)[0];

        if (bidBar.getLow() > chanTop) {
            if (this.sentiment[instrument.ordinal()] != Sentiment.BULL)
                print(instrument.toString() + " Bullish");
            this.sentiment[instrument.ordinal()] = Sentiment.BULL;

        }
        else if (askBar.getHigh() < chanBot) {
            if (this.sentiment[instrument.ordinal()] != Sentiment.BEAR)
                print(instrument.toString() + " Bearish");
            this.sentiment[instrument.ordinal()] = Sentiment.BEAR;

        }
        else {
            if (this.sentiment[instrument.ordinal()] != Sentiment.NEUTRAL)
                print(instrument.toString() + " Neutral");
            this.sentiment[instrument.ordinal()] = Sentiment.NEUTRAL;
        }
    }

// *****  MISC  ****************************************************************

    public enum Sentiment {
        BEAR, BULL, NEUTRAL
    }



// *****  API  ****************************************************************
    public void onStart(IContext context) throws JFException {
        this.engine = context.getEngine();
        this.indicators = context.getIndicators();
        this.history = context.getHistory();
        this.console = context.getConsole();    // allows printing to console
        this.context = context;

        Set instSet;
        instSet = context.getSubscribedInstruments();
        Iterator instIter = instSet.iterator();
        IBar prevBidBar, prevAskBar;
        Instrument instrument;

        this.console.getOut().print("--- Started: ");
        this.console.getOut().print(Instrument.toStringSet(instSet));
        print(" ---");

        while (instIter.hasNext())  
        {
            instrument = (Instrument)instIter.next();
            prevBidBar = this.history.getBar(instrument, PERIODINT, OfferSide.BID, 1);
            prevAskBar = this.history.getBar(instrument, PERIODINT, OfferSide.ASK, 1);
            setSentiment(instrument, prevBidBar, prevAskBar);           
        }

        for (IOrder order : this.engine.getOrders()) 
        {
             drawTrade(order);
        }

    }

    // Represents message sent from server to client application 
    public void onMessage(IMessage message) throws JFException {
        IMessage.Type msgType = message.getType();
        IOrder order = message.getOrder();

        // Draw trades on the chart
        if (msgType == IMessage.Type.ORDER_FILL_OK ||
            msgType == IMessage.Type.ORDER_CLOSE_OK) 
        {
            drawTrade(order);
        }

        // adjust riskPct based on win/lose
        if (msgType == IMessage.Type.ORDER_CLOSE_OK) {
            adjRiskPct(order);
        }       
    }

   public void onAccount(IAccount account) throws JFException {
        this.acctEquity = account.getEquity();
        // Rule 6.2, max. one position only
        this.tradingAllowed = (account.getUseOfLeverage() == 0);
    }

    public void onStop() throws JFException {
        for (IOrder order : engine.getOrders()) 
        {   // loop all orders
            // close non-strategy orders, i.e. margin hedging       
            if (order.getLabel() == "") order.close();              
        }

        print("---Stopped---");
   }
}
// ****************************************************************************

←   newer continue   →