Tutorial 2: a more complex strategy

Let's write a more complex strategy.

What will you learn?

The goal of this tutorial is to define a trend following strategy, based on a Channel Breakout.

We will illustrate more complex orders, especially in the exit conditions, such as limit or stop orders and maximum duration. It also exhibits how and where to locate various calculations. We also introduce the portfolio allocation concept, as we will now allocate a given weight (passed as a parameter) for each asset traded.

What's the strategy logic?

We will enter in a position once we have broken a channel defined by the highest high and lowest low of the past n bars.

What's the code?

We define the parameters of the strategy, which are the channel’s lag, the quantity to buy or sell, the target profit and stop loss thresholds (in percent), and the maximum duration (expressed in number of bars).

int channelLag = 30;
double qtyPct = 0.5;
double limitSellExitTargetProfitPct = 0.1;
double stopSellExitTargetProfitPct = 0.04;
int maxDurationBar = 15;

The onStrategyStart() callback includes preliminary calculations and objects’ definition, in our case, the upper and lower bands of our channel.

@Override
public void onStrategyStart() {
    lowestLowSeries = newIndicator().timeSeries("lowest").draw(RED).get();
    highestHighSeries = newIndicator().timeSeries("lowest").draw(RED).get();
}

The onOpen() callback is used to check the entry conditions and some of the exit conditions. Generally speaking, when one has to set up exit (or entry conditions) able to be modified each bar, he(she) has to locate it within the onOpen() callback. On the contrary, if an exit condition is set up only once, for instance once we have entered in a position, you should locate this behavior within the callback onPositionOpened(). This will be optimal as you go through this behavior only once.

@Override
public void onOpen(OpenPrice openPrice) {
    if (getBars().getCount() >= channelLag) {
        if (hasPosition()) {
            if (++barCount > maxDurationBar) {
                closePosition("Max duration reached");
            }
        } else if (highestHigh < openPrice.getPrice()) {
            buy(calculateQty(), "Entry");
        } else if (lowestLow > openPrice.getPrice()) {
            sell(calculateQty(), "Entry");
        }

        highestHigh = getBars().getHighestHigh(channelLag);
        highestHighSeries.add(openPrice.getDate(), highestHigh);

        lowestLow = getBars().getLowestLow(channelLag);
        lowestLowSeries.add(openPrice.getDate(), lowestLow);
    }
}

This user defined function is used to calculate the exact quantity to buy or sell, based on a given weight qtyPct passed as a parameter.

private long calculateQty() {
    double prevClose = getBars().ago(1).getClose();
    return Math.round(1000000 * qtyPct / (prevClose * getInstrumentFactor()));
}

The onPositionOpened() callback is used to define the “static” exit conditions, ie the stop loss and target profit levels, it is also within this callback that we initialize the counter used for our duration type exit. We will define the "cancelOrderIfNecessary" function later.

@Override
public void onPositionOpened() {
    barCount = 0;

    cancelOrderIfNecessary(stopSellOrder);
    cancelOrderIfNecessary(limitSellOrder);

    long qty = getPosition().getQuantity();
    double entryPrice = getPosition().getEntryPrice();

    if (getPosition().isLong()) {
        double targetProfitExitPrice = entryPrice * (1 + limitSellExitTargetProfitPct);
        limitSellOrder = sellLimit(qty, targetProfitExitPrice, "Exit @ Limit");

        double stopLossExitPrice = entryPrice * (1 - stopSellExitTargetProfitPct);
        stopSellOrder = sellStop(qty, stopLossExitPrice, "Exit @ Stop");
    } else {
        double targetProfitExitPrice = entryPrice * (1 - limitSellExitTargetProfitPct);
        limitSellOrder = buyLimit(qty, targetProfitExitPrice, "Exit @ Limit");

        double stopLossExitPrice = entryPrice * (1 + stopSellExitTargetProfitPct);
        stopSellOrder = buyStop(qty, stopLossExitPrice, "Exit @ Stop");
    }

    limitSellOrder.setOCAGroup(getInstrumentSymbol() + " OCA " + ocaCount);
    stopSellOrder.setOCAGroup(getInstrumentSymbol() + " OCA " + ocaCount);
    ocaCount++;
}

In the onPositionClosed() callback, we delete the pending orders that are no longer valid for our trading, that is : if we went out using everything but the stop loss setup, we have to delete the stop loss related pending order. It means we cancel the order if it is not null and not yet processed or partially filled. The same does apply for the target profit exit.

@Override
public void onPositionClosed() {
    cancelOrderIfNecessary(stopSellOrder);
    cancelOrderIfNecessary(limitSellOrder);
}

private void cancelOrderIfNecessary(Order order) {
    if ((order != null) && (order.isNotYetProcessed() || order.isPartiallyFilled())) {
        cancel(order);
    }
}

What does the result look like?

We apply this strategy on two futures, CAC_40 and CRUDE_OIL, leading to the following result :

screenshot-Tutorial02-equityCurve.png

What's next?

You might want to give a try to our third Tutorial.