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. I now buy long when prices moved down very much, and go short when they moved up very much.
Alice: How much is very much?
Bob: Depends on the market.
Alice: I should have known.
Bob: Well, 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.
Alice: Of course it's more complicated than trend trading. This comes at a higher fee.
Bob: I like that less.
Alice: I can run a Walk Forward Optimization as an extra.
Bob: Huh? Will that get me more profit?
Alice: Not necessarily. But it will make your profit more certain. I want to be sure that you can afford me.

The counter trend algorithm

This is the first version of Alice's counter trend trading script (Workshop5_1 script; agreed fee: $12,000):

function run()
{ BarPeriod = 240; // 4 hour bars // calculate the buy/sell signal vars Price = series(price()); vars Filtered = series(BandPass(Price,30,0.5)); vars Signal = series(FisherN(Filtered,500)); var Threshold = 1.0; // buy and sell Stop = 4*ATR(100); Trail = 4*ATR(100); if(crossUnder(Signal,-Threshold)) enterLong(); else if(crossOver(Signal,Threshold)) enterShort();
// plot signals and thresholds plot("Filtered",Filtered,NEW,BLUE); plot("Signal",Signal,NEW,RED); plot("Threshold1",Threshold,0,BLACK); plot("Threshold2",-Threshold,0,BLACK); PlotWidth = 600; PlotHeight1 = 300; }

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:

BarPeriod = 240;

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:

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.

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 = 1.0;

This line defines a new variable Threshold with a value of 1.0. Alice's intention is to let any Signal value that leaves the +1.0 ... -1.0 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 at the average height of 4 candles:

Stop = 4*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.

Plotting signals

Obviously, these trade rules are somewhat more complicated than the simple lowpass function of the previous lesson. So Alice needs to see how the various series look lke, 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. Below is the resulting chart. Load the script Workshop5_1 and make sure that EUR/USD is selected. Click [Test], then click [Result]:

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.

We can see that the script generates a positive return, although we again have profit bursts followed by long unprofitable periods. But we also note something else: the blue Filtered curve got smaller fluctuations from 2012 on, indicating a lower volatility of the EUR/USD. Apparently the market has changed in 2012. At the same time, the system didn't make profits anymore. Is this just chance or can it be used to improve the strategy by filtering out unprofitable periods? This may be subject to some further examination at a later time. We also notice that the red Signal curve does not have this fluctuation variance because it's normalized.

The first question is whether the system is profitable at all. Had it achieved the same performance in live trading, or is the result just due to a lucky choice of parameters? For getting some information how a system would behave in real trading, a simple backtest is not enough. Alice needs to train the strategy.


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.

Which parameters are adapted in in which way is determined in the script; thus parameter adaption is no separate process, but an integral part of the strategy. For this Alice has added some new commands to the strategy (select Workshop5_2):

function run()
{ set(PARAMETERS); // generate and use optimized parameters BarPeriod = 240; // 4 hour bars LookBack = 500; // maximum time period
// 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); if(crossUnder(Signal,-Threshold)) enterLong(); else if(crossOver(Signal,Threshold)) enterShort(); }

Parameter optimization requires some additional settings at the begin of the script:

BarPeriod = 240;
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. It's usually identical to the maximum time period of functions such as HighPass() or Fisher(). 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 FisherN function to be on the safe side.

The signal calculation algorithm now also looks a little different (changes in red):

vars Filtered = series(BandPass(Price, optimize(30, 20, 40)));
vars Signal = series(FisherN(Filtered, 500));
var Threshold = optimize(1, 0.5, 1.5, 0.1);
Stop = optimize(4, 2, 10) * ATR(100);

Three parameters of the strategy are now optimized. The time period of the BandPass filter is 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 - just the previous time period of the BandPass filter. 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.

The next parameter to be optimized is Threshold. This time optimize is called with 4 numbers. The 4th number, which is optional, is the step value to increase the parameter for every optimization run. So Threshold can now have any value from 0.5 to 1.5 in steps of 0.1. Thresholds for triggerings signals should generally be optimized in fixed steps. If the step width is omitted, Zorro increases the parameter value by 10% for every optimization step.

Alice also optimizes the factor for the stop loss distance between 2 and 10. Note that the parameter order in the script matters when optimizing. Trade entry parameters - in this case, the time period and Threshold - should be optimized first, exit parameters - such as Stop - afterwards. Theoretically, there could be even more parameters to optimize - for instance the number of bars for the ATR function, or the Trail distance. But the more parameters we have, and the larger their range is, the higher is the danger of overfitting the strategy. Overfitted strategies perform well in the simulation, but poor in real trading. We'll look into this soon. Until then, just keep in mind that only few essential parameters should be optimized, and only within reasonable parameter ranges.

For training the strategy, click [Train] and observe what the optimize calls do. During 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).

A click on [Test] reveals that training has improved the strategy. But if Alice would trust this result, she'd make a severe mistake.

Walk Forward Optimization

Alice has used the price data from the last 6 years for optimizing the parameters, and has then used the same price data for testing the result. This establishes a sort of self-fulfilling prophecy and generates too optimistic performance figures (see the discussion of curve fitting bias under Testing). It also has a second problem. In 6 years, markets change and trading strategies must adapt. It is not recommended to trade a trained strategy unchanged for many years; normally the strategy parameters should be re-optimized in regular intervals for adapting them to the market situation. Zorro can do that automatically while live trading, but how can we simulate this in a test and get some realistic prediction of the real trading behavior?

The answer is Walk-Forward Optimization (WFO). Contrary to its name, it's not merely an optimization. It's 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 - and this workshop is the most important one for learning strategy development.

All this requires only adding two lines to the script (Workshop5_3):

StartDate = 2005;
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 below:

Simulation period

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 StartDate to 2005. This way enough data can be collected for getting still the same test period from 2010 to 2015.

After loading the Workshop5_3 script, click [Train]. Training now takes a few minutes because the simulation period is now 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. They are used when you click [Test] after the optimization process. Click [Result] for getting 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.

What have we learned in this workshop?

Next: Portfolio Trading

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