Previous: Branches and Loops

Workshop 4: Trend Trading. Price Series. Backtests.

Prediction is difficult. Especially about the future.
- Niels Bohr

The point of trading is knowing the moment when it's good to buy, good to sell, or good to do nothing. A trade strategy uses market inefficiencies - deviations of the price curves from random data - for predicting future prices and finding the right buying and selling points. That will be the topic of the next workshops. Two things to keep in mind:

► All strategies presented here are meant for educational purposes. They all are designed for simplicity, not for maximum profit or robustness. For really trading such a strategy, you would normally add more entry filter rules, and you would also use a more complex trade exit method than a simple stop. But we'll keep it easy in the workshops.

The backtest results included here can be different to the results you'll get when testing the scripts yourself. That's because you're most likely using a more recent simulation time period, and different spread, commission, and rollover parameters which are updated when connecting to a broker. If not otherwise mentioned, the included scripts are set to a simulation period from 2010-2015; for a different time period modify or remove the StartDate and EndDate lines in the script.

The story of Alice and Bob

The most obvious way to make profits is going with the trend. Here trader Bob tries to explain his trade strategy to programmer Alice. Bob has just hired her to automatize his system, since he wants to replace himself with an algorithm:

Bob: I go with the trend. I buy long when prices start going up and I go short when they start going down.
Alice: 'Going up' means that today's price is higher than yesterday's price?
Bob: Nah, one single higher price alone won't do. Prices wiggle a lot. I look for long term trend, like the trend of the last month. I do that with a moving average. That means I just sum up the prices of the last 4 weeks and take their average.
Alice: Good. That shouldn't be a problem to automatize.
Bob: Well, actually there is a problem. You see, a moving average is not very good for trend finding. For getting a smooth trend curve the moving average period must be made long. But making the average long also lets it lag a lot behind the prices. The signals are just not timely for a good trade. The trend is already over when my moving average finally bends up or down. You need to sort of look ahead of the moving average curve, if you get my meaning.
Alice: So you want to know when a 4 weeks trend changes, but you need to know it in far less time than 4 weeks?
Bob: You got it.
Alice: I could use a lowpass filter for getting the trend curve. Second order lowpass filters have almost no lag. Will that be ok for you?
Bob: I dunno what a second order lowpass filter is. But I trust you.
Alice: Good. So I buy when the trend curve changes its direction? For instance, when it starts to move up from a valley, or down from a peak?
Bob: You got it.
Alice: How do you exit trades?
Bob: When it's the right time. Depends on the market.
Alice: I can exit a long position when entering a short one and vice versa. Does this make sense?
Bob: Yeah, that's what I normally do when I'm not stopped out earlier.
Alice: Stopped out?
Bob: Sure. A trade must be stopped when it's losing too much. We need a stop loss. Or do you want my whole account wiped from some bad trade?
Alice: Certainly not before I got paid. At which price do you place the stop loss?
Bob: Not too far and not too tight. I don't want to lose too much, but I also don't want my trades stopped out all the time.
Alice: So let me guess: it depends on the market?
Bob: You got it.

Following the conversation, Alice wrote this trade strategy script for Bob (Workshop4):

function run()
  vars Price = series(price());
  vars Trend = series(LowPass(Price,500));
  Stop = 4*ATR(100);

  vars MMI_Raw = series(MMI(Price,300));
  vars MMI_Smooth = series(LowPass(MMI_Raw,500));
  if(falling(MMI_Smooth)) {
    else if(peak(Trend))

(If you're not yet familiar with scripts, start with Workshop 1.) We can see that the function is now named "run" and not "main". "run" is also a special function name, but while a main function runs only once, a run function is called after every bar with the period and asset selected with the scrollbars. By default, the bar period is 60 minutes. So this function runs once per hour when Zorro trades.


We're now going to analyze the code line by line. At the begin we notice two strange lines that look similar to var definitions:

vars Price = series(price());
vars Trend = series(LowPass(Price,500));

However these special variables are defined with 'vars' and initialized with a series() function call. We define not a single var here, but a var series - a var 'with a history'. (C++ programmers who peeked into the lite-C headers might have noticed that vars is in fact a double* pointer, but that needs not bother us here). A series begins with the current value of the variable, followed by the value the variable had one bar period before, followed by the value from two bar periods before and so on. Series are mostly used for price curves and other data curves. For instance, we could use a series to take the current price of an asset, compare it with the price from the previous bar, and do some other things dependent on past prices.

For better readability of the code, Alice uses a simple naming convention. Her variable names always begin with an uppercase letter and function names with a lowercase letter. This seems not very important, but such a convention helps a lot reading code. An even better idea - sadly not realized by Alice here - would be to let array or series names always end with a 's' (such as Prices, or Trends) for distinguishing them from single-value variables. Confusing single variables and series is one of the top beginner's mistakes with scripts.

The series() function converts a single variable to a series. The variable or value for filling the series is normally passed as parameter to that function. However, we're not using a variable here, but the return value of another function call instead. vars Price = series(price()); means: define a var series with the name "Price" and fill it with the return value of the price() function. As we've learned in the last workshop, you can this way 'nest' as many function calls as you want, using the return values of functions as parameters to other functions.

The price() function returns the mean price of the selected asset at the current bar. There are also priceOpen(), priceClose(), priceHigh() and priceLow() functions that return the opening, closing, maximum and minimum price of the bar; however, the mean price is usually the best for trade strategies. It's averaged over all prices inside the bar and thus less susceptible to random noise.

vars Trend = series(LowPass(Price,500));

This line defines a series named "Trend" and fills it with the return value from the LowPass function. As you probably guessed, this function is Alice's second order lowpass filter. Its parameters are the previously defined Price series and a time period, which Alice has set to 500 bars. 500 bars are about 4 weeks (1 week = 24*5 = 120 hours). The lowpass filter attenuates all the wiggles and jaggies of the Price series that are shorter than 4 weekss, but it does not affect the trend or long.term cycles above two months. You can see the frequency characteristic of the LowPass filter in the image on the Filter page.

In the image below, the black line is the original EUR/USD price and the red line is the result from the LowPass function:

A lowpass filter has a similar smoothing effect as a Moving Average function (see Indicators), but has the advantages of a better reproduction of the price curve, and less lag. This means the return value of a lowpass filter function isn't as delayed as the return value of a Moving Average function that is normally used for trend trading. The script can react faster on price changes, and thus generate better profit.

Stop loss

The next line places a stop loss limit:

Stop = 4*ATR(100);

Stop is a predefined variable that Zorro knows already, so we don't have to define it. It's the maximum allowed loss of the trade. The position is sold immediately when it lost more than the given value. The limit here is given by 4*ATR(100). The ATR function is a standard indicator. It returns the Average True Range - meaning the average height of a candle - within a certain number of bars, here the last 100 bars. So the position is sold when the loss exceeds the size of four candles. By setting Stop not at a fixed value, but at a value dependent on the fluctuation of the price, Alice adapts the stop loss to the market situation. When the price fluctuates a lot, higher losses are allowed. Otherwise trades would be stopped out too early when the price jumps down just for a moment.

A stop loss should normally be used in all trade strategies. It not only limits losses, it also allows the trade engine to better calculate the risk per trade and generate a more accurate performance analysis.

Filtering trades

Any strategy should have a mechanism for detecting if the exploited price anomaly is present or not. In this case, if the market is trending or not. Many indicators have been invented for detecting trending and non-trending market situations, with more or less success. Alice uses the MMI, the Market Meanness Index, an indiator that performs a statistical analysis of the data.

Alice has calculated the MMI for the last 300 bars and smoothed it with the LowPass filter:

vars MMI_Raw = series(MMI(Price,300));
vars MMI_Smooth = series(LowPass(MMI_Raw,500));

Buying and selling

The following lines are the core of Alice's strategy:

if(falling(MMI_Smooth)) {
  else if(peak(Trend))

Trades are only entered when the smoothed MMI is falling, indicating the begin of a trend. The valley function is a boolean function; it returns either false (i.e. 0) or true (a value different to 0). Here it returns true when the series just had a downwards peak. The peak function returns true when the series just had an upwards peak. The if(..) condition then is then fullfilled, and a long or short trade with the selected asset is entered with a enterLong or enterShort command. If a trade was already open in the opposite direction, it is automatically closed by this command. Note how we combined the else of the first if with a second if; the second if() statement is only executed when the first one was not.

Let's have a look into an example trade triggered by this command:

The red line in the chart above is the Trend series - the lowpass filtered price. You can see that it has a peak at the end of September, so the peak(Trend) function returned true and the enterShort function was called. The tiny green dot is the moment where the short trade was entered. The Trend series continues down all the way until November 23, when a valley was reached. A long trade (not shown in this chart) was now entered and the short trade was automatically closed. The green line connects the entry and exit points of the trade. It was open almost 2 months, and made a profit of ~ 13 cents per unit, or 1300 pips.


Now let's just test how buying and selling works in that strategy. Start up Zorro, select the [Workshop4] script and the [EUR/USD] asset, leave the [Period] slider at 60 minutes, then click [Test]:

Remember that you'll most likely get a different result when testing this strategy in a different time period or when your simulated account has different spread, rollover costs, commission, or slippage. By default, the simulation runs over the last 6 years, f.i. from 2010 until 2015. The strategy achieves an annual profit of ~600 pips, equivalent to about 50% annual return on capital - that's the average profit per year divided by the sum of drawdown and margin. The average monthly income (MI in the window) is 5 $. That's quite modest, but Zorro simulated a microlot account and needs only ~100 $ capital for keeping this strategy at a safe distance from a margin call. So you have about 5% return on capital per month. By the way, the '$' sign in Zorro's messages does not necessarily mean US-Dollars, it represents the account currency.

The equity curve can be seen by clicking [Result]:

In the image that pops up in the chart viewer, you can see a black curve and some green lines and red dots attached to the curve. In the background there's a jaggy blue area and a red area below. The black curve is the price of the selected asset - the EUR/USD, i.e. the price of 1 EUR in US Dollars. The price scale is on the left side of the chart. The green and red dots are winning and losing trades. The green lines connect the entry and exit point of a winning trade. You can see immediately that there are far more red than green dots - about 80% of the trades are lost. However, the long-term trades all have green lines. So we have a lot of small losses, but several large wins. This is the typical result of a trend following strategy.

The most interesting part of the chart is the blue area that represents the equity curve. We can see that it's below zero in the first years, then rises to about 200 $ in 2014. The red area below is it's evil counterpart, the "underwater equity" or drawdown curve that indicates losses on our account. The more blue and the less red, the better is the strategy. This one gives a mixed result. The winning years are 2010 and 2011; in the other years we had a loss or a tie. This shaky behavior is reflected in the low Sharpe Ratio and the high Ulcer Index (UI) of the strategy.

Analyzing the trades

Since Alice had wanted to check the single trades of this strategy in detail, she had added the following line to the run function:


LOGFILE is a flag - something like a "switch" that can be on or off. Such switches are turned on with the set() function. If the switch is on, the next click on [Test] stores a log of all events in the Log subfolder. Add the line, click [Test], then open Log\Workshop4_EURUSDtest.log with the script editor or any other plain text editor. It begins with a list similar to this one:

BackTest: Workshop4 EUR/USD 2008..2013
[139: Thu 10.01. 07:00]  1.46810
[140: Thu 10.01. 08:00]  1.46852
[141: Thu 10.01. 09:00]  1.46736
[142: Thu 10.01. 10:00]  1.46721
[EUR/USD::S4300] Short 1@1.4676 Risk 6 at 10:00
[143: Thu 10.01. 11:00]  0p 0/1
[EUR/USD::S4300] Reverse 1@1.4694: -1.52 at 11:00
[EUR/USD::L4400] Long 1@1.4694 Risk 6 at 11:00
[144: Thu 10.01. 12:00]  -20p 0/2
[145: Thu 10.01. 13:00]  -20p 0/2
[146: Thu 10.01. 14:00]  -20p 0/2
[EUR/USD::L4400] Reverse 1@1.4649: -3.54 at 14:00
[EUR/USD::S4700] Short 1@1.4649 Risk 6 at 14:00
[147: Thu 10.01. 15:00]  -67p 0/3
[EUR/USD::S4700] Stop 1@1.4729: -6.22 at 15:00
[148: Thu 10.01. 16:00]  -148p 0/3
[EUR/USD::L4900] Long 1@1.4744 Risk 6 at 16:00

The meaning of the messages is explained in the trading chapter. Let's go through the log above. The first short trade is entered after bar number 142, starting January 10 10:00 am. Zorro bought 1 lot EUR/USD at a price of $1.4676. The trade risk was about $6, determined by the stop loss distance (due to slippage the risk can be estimated only). After one bar the short trade was closed at a loss of $1.52, the position was reversed, and a long trade was opened at a price of $1.4694. It was closed 3 bars later at a $3.54 loss.

You can see in the log that most trades are lost. Zorro seems to deliberately enter trades in the wrong direction; trading at random would only lose a little more than 50%, not 80%. Surely no human trader in his right mind would enter trades this way! But there's a method behind this madness. The algorithm wants to be in a favorable position when a long-term trend begins, and then keeps the position for a long time. That's why it wins in the long run despite losing most trades.

The trade distribution

For some more insight into the distribution of trades, Alice wants to plot the trade distribution. For this she adds the following line to the very begin of the script (before the run function):

#include <profile.c>

This is a command to the compiler to insert another script file from the include folder. profile.c is a script that contains functions for plotting price and trade statistics and seasonal analysis charts. For the trade distribution, Alice calls the following function from inside the run function:


This generates a trade distribution chart in steps of 50 pips at the end of the test run. The generated chart looks similar to this one:

For generating the chart, all trades are sorted into buckets, depending on their profit. Every bucket is represented by a red and a blue bar. Trades with a loss between -200..-100 pips go into the first bucket at the left side, marked -200 at the x axis. The next buckets are for trades with loss or profit from -200..-150 pips, -150..-100 pips, -100..-50 pips, and so on. The height of the blue bar is the number of trades ending up in that bucket (right y axis), the height of the red bar is the sum of all profits in the bucket (left y axis). We can see that most trades end with a loss between 0 and -50 pips. The total profit of the system comes from relatively few profitable trades, some even with about 1000 pips profit.

What have we learned in this workshop?

Next: Strategy Optimizing

Further reading: ► series, price, filters, stop, buy, sell, plot, performance, LOGFILE, #include, profile

Trading essentials: ►Trading101, Bars, Strategies, Indicators