Previous: Portfolio Trading

Workshop 7: Machine Learning. Price Action Trading

Bob: Quick, close the door! Maybe I've been shadowed.
Alice: What happened?
Bob: Someone just revealed the ultimate trading secret to me.
Alice: Really?
Bob: Yes. It's a price action trading method. I need you to program this immediately.
Alice: Price action? What's that?
Bob: You don't use any indicators. You trade just when the price candle pattern is right. You compare the open, high, low and close of the last candle with the open, high, low and close of the previous candles - that's the pattern. It's all you need.
Alice: Didn't the Japanese use such candle patterns for rice trading, with funny names like "Three Black Crows"?
Bob: Yeah, 300 years ago. I don't think that Japanese rice candle patterns are still good for trading today. But I know a guy named Bert at McDuck Capital. He found new candle patterns that work for the Forex market. He said it's like a slot machine. The pattern appears and cash comes out. Bert got a mad bonus and McDuck is since then trading price action with his patterns.
Alice: So you want me to write a script that looks for those patterns and then triggers a trade signal? Should not be a problem.
Bob: Well, there is a problem. I don't know the patterns. I only know that they are made of three daily candles.
Alice: You don't know of which candles?
Bob: No, Bert said he had to kill me when he told me that. McDuck is very serious in that matter.
Alice: Hmm.
Bob: Can't you find out the patterns yourself?
Alice: If a guy at McDuck found them, I suppose I can find them too. But why do they work at all? I mean, why should a price move be preceded by a certain candle pattern?
Bob: No idea. But this method worked for the Japanese rice market. Maybe there are some big traders that wake up in the morning, look at the last 3 day candles, and if they like what they see they buy or sell.
Alice: If this establishes a pattern, I can apply a machine learning function. It goes through historic prices and checks which candle patterns usually precede a price movement up or down.
Bob: Will that be expensive?
Alice: The search of candle patterns? No. Still, I'm afraid I'll have to charge more than last time.
Bob: Why is that?
Alice: Risk fee. I might get killed when programming this script.

The price action script

Alice uses Zorro's advise function for finding a system in candle patterns, and using the most profitable patterns for a trade signal. This is her script (Workshop7, fee incl. risk: $30,000):

function run()
  StartDate = 2005;   // use > 10 years data
  BarPeriod = 1440;   // 1 day
  BarZone = WET;      // West European midnight
  Weekend = 1;        // separate Friday and Sunday bars
  LookBack = 3;       // only 3 bars needed
  NumWFOCycles = 10;   // mandatory for AI functions
  set(RULES+TESTNOW); // generate rules, test after training
  if(Train) Hedge = 2; // allow long + short
  ExitTime = 6;       // = one week
    priceHigh(0),priceLow(0),priceClose(0)) > 40)
  if(adviseShort() > 40)

Many lines in this code should be already familiar, but there are also some new concepts. Let's start with the most important one, the machine learning algorithm.

The advise function

This looks like a strange entry condition for a long trade:

if (adviseLong(PATTERN+2,0,
  priceHigh(0),priceLow(0),priceClose(0) ) > 40)

Alice calls adviseLong with the PATTERN classification method and the High, Low, and Close prices of the last 3 candles. If adviseLong returns a value above 50, a long trade is entered (reverseLong is a 'special version' of enterLong; we'll come to that soon) . But when does this happen?

In training mode, the adviseLong function always returns 100. So a trade is always entered. The function stores a 'snapshot' of its signal parameters - in this case, 12 signals from the High, Low, and Close prices of the last 3 candles - in an internal list. It then waits for the result of the trade, and stores the profit or loss of the trade together with the signal snapshot. Thus, after the training run Zorro got a large internal list containing all signal snapshots and their corresponding trade profits or losses.

The signals are then classified into patterns. Alice has put a +2 to the PATTERN parameter. This tells Zorro's pattern analyzer to split the signals into two equal groups. Each got 6 of the 12 signals. The first group contains the prices of the first two candles of the 3-candle sequence:


And the second group contains the prices of the last two candles:


Note that the middle candle, with offset 1, appears in both groups. The Open price is not used in the signals because currencies are traded 24 hours a day, so the Close of a daily bar is normally identical to the Open of the next bar. Using the Open price would emphasize outliers and weekend patterns, which is not desired.

Within every signal group, Zorro now compares every signal with every other signal. This generates a huge set of greater, smaller, or equal results. This set of comparison results is a pattern. It does not matter if priceHigh(2) is far smaller or only a little smaller than priceHigh(1) - the resulting pattern is the same. The patterns of the two groups are now glued together to form a single pattern. It contains all information about all price comparisons within the first and the second and within the second and the third candle, but the pattern does not contain any information about how the first candle compares with the third. Bert had told Bob that it's best for price action trading to compare only adjacent candles - therefore the two independent pattern groups. If Alice had looked for 4-candle-patterns, she'd used three groups.

Aside from grouping, Zorro makes no assumptions of the signals and their relations. Therefore, in stead of candle patterns any other set of signals or indicators could also be used for the advise function. After the pattern was classified, Zorro checks how often it appears in training data set, and sums up all its profits or losses. If a pattern appears often and with a profit, it is considered a profitable pattern. Zorro removes all unprofitable or insignificant patterns from the list - patterns that don't have a positive profit sum or appear less than 4 times. From the remaining patterns, a pattern finding function is generated and stored in the workshop7_EURUSD.c script in the Data folder. Such a machine generated pattern finding function can look similar to this:

int EURUSD_L(float* sig)
  if(sig[1]<sig[2] && eqF(sig[2]-sig[4]) && sig[4]<sig[0] && sig[0]<sig[5] && sig[5]<sig[3]
    && sig[10]<sig[11] && sig[11]<sig[7] && sig[7]<sig[8] && sig[8]<sig[9] && sig[9]<sig[6])
      return 19;
  if(sig[4]<sig[1] && sig[1]<sig[2] && sig[2]<sig[5] && sig[5]<sig[3] && sig[3]<sig[0] && sig[7]<sig[8]
    && eqF(sig[8]-sig[10]) && sig[10]<sig[6] && sig[6]<sig[11] && sig[11]<sig[9])
      return 70;
  if(sig[1]<sig[4] && eqF(sig[4]-sig[5]) && sig[5]<sig[2] && sig[2]<sig[3] && sig[3]<sig[0]
    && sig[10]<sig[7] && eqF(sig[7]-sig[8]) && sig[8]<sig[6] && sig[6]<sig[11] && sig[11]<sig[9])
      return 74;
  if(sig[1]<sig[4] && sig[4]<sig[5] && sig[5]<sig[2] && sig[2]<sig[0] && sig[0]<sig[3] && sig[7]<sig[8]
    && eqF(sig[8]-sig[10]) && sig[10]<sig[11] && sig[11]<sig[9] && sig[9]<sig[6])
      return 43;
  if(sig[1]<sig[2] && eqF(sig[2]-sig[4]) && sig[4]<sig[5] && sig[5]<sig[3] && sig[3]<sig[0]
    && sig[10]<sig[7] && sig[7]<sig[8] && sig[8]<sig[6] && sig[6]<sig[11] && sig[11]<sig[9])
      return 68;
  return 0;

Machine generated code is automatically called by adviseLong/Short in test or trade mode, or when training other parameters. The functions in the code get their names from their asset, their algo, and whether they are used for long or short trades. This way many different functions can be stored in the same .c file. The function here has the name EURUSD_L. The list of signals is passed to the function as a float array named sig. float is a variable type similar to var, but has lower precision and thus consumes less memory. sig[0] is the first signal passed to the advise function - in this case priceHigh(2). sig[1] is the second signal (priceLow(2)) and so on. The signals are accessed inside the function just like the elements of a series.

We can see that the function contains many if() conditions with many comparisons of signals with other signals. Any if() condition represents a pattern. The comparisons are linked with && (that's the same as the and operator), so the if() condition is true only when all its comparisons are true. In this case a certain value, like 19 in the first if() condition in the above example, is returned by the function. If none of the if() conditions is true, 0 is returned, meaning that no pattern is found. The returned value is the pattern's score - its information ratio multiplied with 100. The higher the information ratio, the more predictive is the pattern.

Alice compared the returned value with 40, meaning that a long trade is entered for any pattern match with a score above 40. Short trading just works the same way:

if(adviseShort() > 40)

The adviseShort call has no parameters. In that case the function uses the same method and signals as the last advise call, which was the preceding adviseLong. This way lengthy signal lists don't have to be written twice.

Limiting the number of trades

For reducing drawdown and risk, Alice wants to open only one trade at a time. If a profitable pattern is detected and a trade in the pattern direction is already open, no more trades should be entered. One way to achieve this is evaluating the NumOpenLong or NumOpenShort variables. So Alice would enter a trade only when the number of open trades is less than the limit. This would require an extra conditon in the trade signal, like this:

if (adviseShort() > 40 and NumOpenShort < 1)

However, this method would not close trades in opposite direction when the condition is not fulfilled. It also just keeps the previous trades open, but it would be better to update them with the stop loss or take profit limit from the new trade, as if they were replaced by the new one. And finally, it would also limit the trades in training mode, which means training would miss many patterns. Taking care of all this requires more code.

Because this is a frequent task, a helper function reverseLong / reverseShort has been written for this. This function can be found in the include\default.c file. This way it is automatically included and can be called in all strategies. It first establishes a pending exit for trades in the same direction, this way effectively updating the stop loss and take profit limits of open trades. It also updates the trade exit time. Then it checks if the trade number limit is reached. If not, it opens a new trade, otherwise it simulates a reversal by explicitly closing all trades in opposite direction. For using it, just replace enterLong() with reverseLong(1) and enterShort() with reverseShort(1).

The only parameter to the functions is the maximum allowed number of open trades. In this case, Alice allows only one trade at the same time, so the parameter is 1.

Price action set-up

There are still some prerequisites for pattern analysis that haven't been discussed yet. Let's look at the rest of the code:

StartDate = 2004;
BarPeriod = 1440;
NumWFOCycles = 10;

NumWFOCycles or some other out-of-sample test method is mandatory for this type of strategy. All machine learning systems tend to overfitting, so any in-sample result from price patterns, decision trees, or preceptrons would be far too optimistic and thus meaningless. The number 10 is a compromise: higher numbers produce more WFO cycles, ergo less bars for any cycle to train, so less patterns are found and the results become more random. Lower numbers produce more bars per cycle and more patterns are found, but they are from a longer time period - above one year - within which the market can have substantially changed. So the results can become more random, too.

The number of bars from the same time period could theoretically be increased with oversampling. Unfortunately, oversampling is useless for daily bars because the High, Low, and Close prices depend on a certain bar start and end time. Resampled bars would produce very different patterns. So Alice has to use more than 10 years for the simulation period to get enough data for training. (If price data from a certain year is not included in the Zorro program, it can be downloaded either automatically from the broker server, or with the historic price package from the Zorro download page).

BarZone = WET;

Normally, daily bars begin and end at UTC midnight. But for price patterns the time zone of the bars is critical. Alice has to catch a time with low volatility. A good time for low EUR/USD volatility is midnight in Western Europe. BarZone determines the time zone of a daily bar; WET is the Western European Time, the time zone of London, considering daylight saving time.

Weekend = 1;

The Weekend variable determines how the simulator deals with weekend bars. Normally, no bar is allowed to start or end within a weekend. This means that for daily bars, the bar starting Friday 00:00 midnight would end Monday 00:00 midnight. This is not desired here because this bar would then contain prices from Friday as well as from Sunday evening, and spoil the candle pattern. Thus, Weekend = 1 enforces the Friday bar to end Saturday 00:00 midnight, although due to the weekend no trades can be entered on that bar. The week then consists of 6 instead of 5 bars.

LookBack = 3;

Because the strategy needs only the last 3 candles for trade decisions, we can set the lookback period from its default 80 down to 3 bars. This gives us three months more for training and testing.


The RULES flag is required for generating price patterns with the advise function. TESTNOW runs a test automatically after training - this saves a button click when experimenting with different pattern finding methods.

The next code part behaves different in training and in test or trade mode:

if(Train) Hedge = 2;

Train is true in [Train] mode. In this mode we want to determine the profitability of a trade that follows a certain pattern. Hedge is set to 2, which allows long and short positions at the same time. This is required for training the patterns, otherwise the short trade after adviseShort would immediately close the long positions that was just opened after adviseLong, and thus assign a wrong profit/loss value to its candle pattern. Hedge is not set in test and trade mode where it makes sense that positions are closed when opposite patterns appear.

ExitTime = 6;

ExitTime sets the duration of a trade to 5 bars, equivalent to one week. If a trade is not closed by an opposite pattern, it is closed after a week. The trade results after one week are also used for training the candle patterns and generating the trade rules.

The result

Click [Train]. Depending on the PC speed, Zorro will need a few seconds for running through the five WFO cycles and finding about 50 profitable long or short patterns in every cycle. Click [Result] for the equity curve:

The machine learning algorithm with daily candle patterns seems to give us a relatively steadily rising equity curve and symmetric results in long and short trading. This was apparently the top secret result that Bert presented to his employer. But was his mad bonus justified? Or was his employer just fooled by randomness? Find out by running the test many times with a randomized price curve (Detrend = SHUFFLE), plotting a histogram of the results, and comparing it with the result from Alice's test. How to do such an histogram will be explained in the next workshop.

What have we learned in this workshop?

Next: Anatomy of a Scam Robot

Further reading: ► advise, RULES