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---");
   }
}
// ****************************************************************************