Previous: Trend Trading

Workshop 5: Counter-Trend Trading. Walk Forward Analysis.

Bob: Last month I ran into Warren Buffett and asked him for a trading advice. That's what he said: 'Be greedy when others are fearful'.
Alice: Interesting. And what does it mean?
Bob: He didn't tell, but went away. But I think he wants me to go against the trend.
Alice: Isn't that just the opposite of your last strategy?
Bob: You got it. I need you to automatize this. Prices often move up and down in cycles. You must check if the price is close to the bottom or top of a cycle. This is the right moment to buy or sell.
Alice: I can use a bandpass filter for cutting off the trend and the noise, and getting a clean price cycle.
Bob: Sounds good.
Alice: For finding how close the cycle is to its peaks, I can normalize it to a fixed range. And then apply a Fisher transformation for giving the it a Gaussian distribution. This makes the peaks of the cycle relatively sharp and well defined, so we won't get too many false signals.
Bob: I have no idea what you're talking about. But when it's sharp and well defined, I like it.

The counter trend algorithm

This is Alice's counter trend trading script (Workshop5):

function run()
  BarPeriod = 240;  // 4 hour bars
  LookBack = 500;   // maximum indicator time period
  set(PARAMETERS);  // generate and use optimized parameters
  StartDate = 2005;
  NumWFOCycles = 10;
  if(ReTrain) {
    UpdateDays = -1; 
    SelectWFO = -1; 
// calculate the buy/sell signal with optimized parameters
  vars Price = series(price());
  vars Filtered = series(BandPass(Price,optimize(30,20,40),0.5));
  vars Signal = series(FisherN(Filtered,500));
  var Threshold = optimize(1,0.5,1.5,0.1);
// buy and sell
  Stop = optimize(4,2,10) * ATR(100);
  Trail = 4*ATR(100);
  else if(crossOver(Signal,Threshold))
// plot signals and thresholds
  PlotWidth = 600;
  PlotHeight1 = 300;

Let's again go through the script line by line:

BarPeriod = 240;

Counter trend trading is affected by market cycles and more sensitive to the bar period than trend trading. Bob has told Alice that bar periods that are in sync with the worldwide markets - such as 4 or 8 hours - are especially profitable with this type of trading. Therefore she has set the bar period to a fixed value of 4 hours, or 240 minutes:

LookBack = 500;

PARAMETERS is a flag - similar to the LOGFILE flag that we know from the last workshop - that tells Zorro to generate and use optimized parameters. LookBack must be set to the 'worst case' lookback time of the strategy. The lookback time is required by the strategy for calculating its initial values before it can start trading. If the lookback time depends on an optimized parameter, Zorro can not know it in advance; so we should make it a habit to set the LookBack variable directly when we optimize a strategy. In this case we set it to the 500 bars required for the later used FisherN function to be on the safe side.

The counter trend trade rules are contained in the following lines that calculate the buy/sell signal. The first line sets up a price series just as in the trend trading strategy:

vars Price = series(price());

In the next line, a bandpass filter is fed with the price curve. Let's set aside the optimize function for the moment. Without optimization, the line would look like this:

vars Filtered = series(BandPass(Price, 30, 0.5));

This bandpass filter has a center period of 30 bars and a width of 0.5. The BandPass function is similar to the LowPass function except that it also dampens high frequencies, i.e. short cycles. Its frequency curve can be examined in the Filters chapter. This way the trend (a cycle with a very long period) and the noise (short period cycles) are removed from the price curve. The result is a clean curve that consists mostly of the medium-period peaks and valleys. It's stored in a new series named Filtered.

The center period it the first parameter that Alice wants to optimize, so the line reads:

vars Filtered = series(BandPass(Price, optimize(30, 20, 40)));

The center period of the BandPass filter is now set from the return value of an optimize function. We notice that optimize is called with 3 numbers. The first is the parameter default value, which is 30. The next two numbers, 20 and 40, are the parameter range, i.e. the lower and upper limit of the time period. So the BandPass time period will now run from 20 to 40. During the optimization process, Zorro will try to find the most robust time period within this range.

Just like the original prices, the values of the Filtered price curve are still all over the place. For generating a trade signal they must be normalized - meaning they are 'compressed' in a defined range so that they can be compared with a threshold. In traditional technical analysis, an indicator called "Stochastic" is used for normalizing a curve. Alice prefers the Fisher Transformation. This is an operation that transforms a curve into a Gaussian distribution - that's the famous 'bell curve' distribution where most values are in the center and only few values are outside the +1...-1 range. Normalization and Fisher transformation are done with the FisherN function. It converts the Filtered series into the normalized and Gaussian distributed Signal series, using the last 500 bars for the normalization.

vars Signal = series(FisherN(Filtered, 500));

The Signal series can now finally be compared with an upper and lower threshold for generating trade signals. The Threshold is defined in the next line:

var Threshold = optimize(1,0.5,1.5,0.1);

This line defines a new variable Threshold with a value that is optimized between 0.5 and 1.5 in steps of 0.1. Alice's intention is to let any Signal value that exceeds the thresold range trigger a trade.

This happens in the following part of the code. But before we can start trading, Alice places a stop loss at an adaptive distance from the price, just as in the trend trading script. The ATR function is again used to determine the stop loss distance with an optimized factor:

Stop =optimize(4,2,10) * ATR(100);

Additionally to the stop loss, Alice has also placed a trail limit 4 average candles away from the current price:

Trail = 4*ATR(100);

If the trade now goes in favorable direction by more than 4 average candles, the stop loss will follow the price at a distance of 8 candles. This ensures that all trades that reach an 8 candle profit are guaranteed to end with a win, regardless how the price further behaves. Trailing often - not always - improves the profit of a strategy, but is almost always better than placing a profit target.

if(crossUnder(Signal, -Threshold))
else if(crossOver(Signal, Threshold))

When the Signal curve crosses the negative threshold from above - meaning when Signal falls below -1 - the price is supposedly close to the bottom of the main cycle, so we expect the price to rise, and buy long. When the threshold is crossed from below - meaning Signal rises above 1 - the price is close to a peak and we buy short. This is just the opposite of what we did in trend trading. For identifying the threshold crossing we're using the crossOver() and crossUnder() functions.

Training with Walk Forward Optimization

Training serves two purposes. At first, it improves the 'robustness' of a strategy. During the training run, strategy parameters are optimized and adapted to the market until the strategy returns stable profits with minimum deviation. The second purpose is finding out how sensitive the system is to small parameter changes. The more sensitive, the less likely is it that backtest results are reproduced in live trading.

Alice used Walk-Forward Optimization (WFO), which is not merely an optimization, but an analysis method that tests the strategy together with its parameter ranges and optimization method. If a strategy fails in a walk forward analysis, it will also fail in real trading, even if it collected huge profits in backtests. For this reason, walk forward optimization is the most important process when developing a strategy.

WFO is activated by this line in the script:

NumWFOCycles = 10;

This activates WFO with a data frame that is shifted in 10 cycles over the simulation period. The frame consists of a training period and a subsequent test as in the figure here. The lookback periods at the begin are needed to collect initial data for the functions. The training periods generate the parameters that are then tested in the subsequent test periods. This ensures that every test uses "unseen" price data that were not used for optimizing its parameters - just as in real trading. The data frame is then shifted over the simulation period for verifiying how the strategy would fare when started at different times.

Because the test period is now much smaller than the whole simulation period, Alice has set a far backStartDate = 2005.. This way enough data can be collected for getting a test period from 2010 to 2015.

For training the strategy, click [Train] and observe what the optimize calls do. It now takes a few minutes because the simulation period is about 10 years, and a full optimization is performed for any of the 10 cycles. The optimized parameters are stored in a separate parameter file for every WFO cycle. After the training phase, which can take about one minute depending on the PC speed, you'll see some charts pop up, like this:

Parameter 1 (time period)
Parameter 2 (Threshold)
Parameter 3 (Stop factor)

The parameter charts show how the parameter values affect the performance of the strategy. The red bars are the return ratio of the training period - that's basically the total win divided by the total loss, multiplied by a penalty factor for less trades. The dark blue bars are the number of losing trades and the light blue bars are the number of winning trades. We can see that the time period produces slightly increasing returns up to about 35, then the returns go down. Threshold has the best performance at about 1.0. The stop factor - the third parameter - slightly goes up and has a maximum at about 7. We can also see here that a distant stop, although it increases the risk and eventually reduces the profit, achieves a higher number of profitable trades and thus a better 'accuracy' of the strategy.

All red bars ending above 1.0 indicate a profitable parameter combination. In this case they stay above 1.0 over the whole range, which means that the strategy performance is quite robust and not very sensitive to parameter changes. The optimized parameters are stored in the file Data/Workshop5_2_EURUSD.par (the file name would be different for other assets).

After training, click [Test]. The data from the training is now used for a walk forward analysis. This is the equity curve:

We can see that the strategy still stays profitable with walk forward analysis, but the equity curve does not look smooth and the return in 2013 and 2015 was negative. In the next workshop we'll learn how to make strategy returns more steady and reliable so that Bob can really derive a regular income from them.

Real time optimizing a WFO strategy

When trading a walk forward optimized strategy, it must be regularly re-trained and adapted to the current market situation, just as in the WFO process. For this, Alice has added the following lines to the script:

if(ReTrain) {
  UpdateDays = -1;
  SelectWFO = -1;

ReTrain is nonzero when the [Train] button is clicked during live trading. UpdateDays is the time period for automatically downloading new price data for the current asset from the broker's server, which is then used for the subsequent training cycle. If set to -1, the price data is updated to the current date. Alternatively, price data from a Zorro update could be manually copied into the History folder. SelectWFO tells Zorro not to optimize the whole simulation period, but only a certain WFO cycle; in this case, the last cycle (-1) that contains the new price data.

Clicking [Train] every couple of months (at least every 35 weeks, as indicated by "WFO test cycles" in the performance report) will continue the WFO process during trading, and make the strategy independent of external parameter settings. This way we have essentially a 'parameter-free' strategy.

Plotting signals

Since the trade rules are somewhat more complicated than the simple lowpass function of the previous lesson, Alice needs to see how the various series look like, for checking if everything works as supposed. This happens in the last lines at the end of the script.


This line generates a plot of the Filtered series. It's plotted in a NEW chart window with color BLUE. We can use the plot function to plot anything into the chart, either in the main chart with the price and equity curve, or below the main chart in a new window.

The Signal curve and the upper and lower Threshold are plotted in another new chart window:

plot("Signal", Signal, NEW, RED);
plot("Threshold1", Threshold, 0, BLACK);
plot("Threshold2", -Threshold, 0, BLACK);

The first statement plots the Signal series as a red curve. The next two statements plot the positive and negative Threshold with two black lines in the same chart window.

PlotWidth = 600;
PlotHeight1 = 300;

This just sets the width and height of the chart window. The signals are plotted below the chart:

The blue curve in the middle window is the plot of the Filtered series. It shows the price fluctuation in the range of about +/-0.005, equivalent to about 50 pips. The bottom window displays the Signal series. The black lines are the thresholds that trigger buy and sell signals when Signal crosses over or under them. Plotting variables and series in the chart greatly helps to understand and improve the trade rules. For examining a part of the chart in details, the PlotDate and PlotBars variables can be used to 'zoom into' a part of the chart.

What have we learned in this workshop?

Next: Portfolio Trading

Further reading: ► Training, plot, signal processing, optimize, NumWFOCycles, NumSampleCycles