Previous: Optimizing

Workshop 6: Portfolio strategies. Money management.

Bob: I got it! This is it!
Alice: Bob! Why are you running naked through the streets?
Bob: Huh? Oh, I was just sitting in my bathtub when I suddenly got this brilliant idea. I need you to program it immediately. This will solve all my trading problems!
Alice: Problems? I thought you were already getting rich by automated trading?
Bob: It started well, but then it went all wrong. You know, from all the systems you programmed for me, the counter trend system worked best.
Alice: I figured that.
Bob: And from all assets, I found it had the most profit with the EUR/USD. So I invested all my money in that system trading with EUR/USD.
Alice: Oh no!
Bob: Yes! And it worked like a charm! My account went up from $100,000 to $350,000 in six months. I naturally began to think about what do do with all that money. I had already ordered a Porsche and a golden bathtub.
Alice: Naturally.
Bob: But then all of the sudden, your system started losing money. My account went down and down and didn't stop going down.
Alice: How much?
Bob: It lost $200,000 in a single month. I'm now back at $150,000, and it's staying there since weeks.
Alice: Wins and losses follow a random sequence. Any system will eventually meet a loss streak of any length and depth - that's mathematically certain.
Bob: I know, I know, it's a drawdown and I must just sit it out. But I don't have the nerve for that. Every morning I'm looking at the PC screen and my account is either going down, or not going up!
Alice: Put a blanket over your PC.
Bob: I got a better idea. That's why I was on the way to you. What if the system traded with many assets at the same time?
Alice: Indeed, that could improve the Sharpe Ratio when the assets uncorrelated.
Bob: I have no idea what you mean with that. But when EUR/USD is in a drawdown, chances are that another pair, like USD/JPY, is still profitable. So I lose with one but win with the other. And I can do this with a portfolio of many assets.
Alice: Certainly.
Bob: And can't you also combine your systems into one? I mean a super system that goes with the trend and against the trend at the same time? So I win when assets are trending and I also win when assets are cycling? Meaning I win all the time?
Alice: You probably won't win all the time because I know that different price curves are still somewhat correlated. But it can reduce your drawdowns.
Bob: Well, then program it. I'll sell my Porsche and pay you well.
Alice: Good. I'll start at once. Now better go home and put some clothes on.

The portfolio script

Bob has given Alice the task to write a script that trades with several assets simultaneously, and uses both trend and counter-trend strategy algorithms. This is her first version (Workshop6_1 script, agreed fee: $28,000):

// Counter trend trading function from Workshop 5
function tradeCounterTrend()
{
  TimeFrame = 4;	// 4 hour time frame
  vars Price = series(price());
  vars Filtered = series(BandPass(Price,optimize(30,25,35),0.5));
vars Signal = series(Fisher(Filtered,500));
var Threshold = optimize(1,0.5,2,0.1); Stop = optimize(4,2,10) * ATR(100);
Trail = 4*ATR(100);
if(crossUnder(Signal,-Threshold)) enterLong(); else if(crossOver(Signal,Threshold)) enterShort(); } // Trend trading function from Workshop 4 function tradeTrend() { TimeFrame = 1; // 1 hour time frame vars Price = series(price()); vars Trend = series(LowPass(Price,optimize(500,300,700)));
Stop = optimize(4,2,10) * ATR(100);
Trail = 0;
vars MMI_Raw = series(MMI(Price,300)); vars MMI_Smooth = series(LowPass(MMI_Raw,500)); if(falling(MMI_Smooth)) { if(valley(Trend)) enterLong(); else if(peak(Trend)) enterShort(); } } function run() { set(PARAMETERS); // use optimized parameters BarPeriod = 60; // 1 hour bars LookBack = 2000; // needed for Fisher() StartDate = 2005; // > 10 years NumWFOCycles = 10; // activate WFO, 10 cycles if(ReTrain) { UpdateDays = -1; SelectWFO = -1; } // double portfolio loop while(asset(loop("EUR/USD","USD/JPY"))) while(algo(loop("TRND","CNTR"))) { if(Algo == "TRND") tradeTrend(); else if(Algo == "CNTR") tradeCounterTrend(); } }

The strategy is now divided into 3 different functions: tradeTrend for trend trading, tradeCounterTrend for counter trend trading, and the run function that sets up the parameters, selects the assets, and calls the two trade functions.

The tradeCounterTrend function is almost the same as in the last workshop. At the begin of the function Alice has added the line

TimeFrame = 4;

What does this mean? The trend trading strategy from workshop 4 used 60-minutes bars, while counter trend trading was based on 240-minutes bars. When we trade both together in the same script, we'll need both types of bars. The BarPeriod variable must not be changed at runtime, as it determines the sampling of the price curves. But the TimeFrame variable does the job. Alice has set BarPeriod to 60 minutes, so it needs a TimeFrame of 4 bars for getting the 240 minutes period for the counter trend strategy. TimeFrame affects all subsequent price and series calls, all indicators using those series, and the ATR function.

The tradeTrend function uses the same algorithm as in workshop 4. The TimeFrame variable is set to 1, so this strategy is still based on a 60-minutes bar period. The LowPass time period and the stop loss value are now optimized with the method explained in the last workshop. Alice has also explicitly set the Trail variable to 0:

Trail = 0;

Trend trading works best when profitable trades last a long time; trailing would stop them too early. However, the Trail variable was already set in the tradeCounterTrend function. If Alice had not reset Trail to 0 (meaning no trailing), it would keep its last value, tradeTrend would trail too, and its profits would go down. When predefined variables are used anywhere in the script, make sure that they have the right value at any place where they are needed. This is a typical source of mistakes, so always look over the trade log as described in workshop 4 to be sure that trades behave as they should.

The first lines of the run function are similar to the workshop 5 script, only BarPeriod is now at 60 and consequently LookBack at 2000 for getting the same 500 4-hour periods.

The core of the strategy looks very unfamiliar:

while(asset(loop("EUR/USD","USD/JPY")))
while(algo(loop("TRND","CNTR")))
{
  if(Algo == "TRND")
    tradeTrend();
  else if(Algo == "CNTR")
    tradeCounterTrend();
}

We have two 'nested' while loops, each with two nested functions. Let's untangle them from inside out:

loop("EUR/USD","USD/JPY")

The loop function takes a number of arguments, and returns one of them every time it is called. On the first call it returns the first parameter, which is the string "EUR/USD". We have learned in Workshop 1 that a string is a variable that contains text instead of numbers. Strings can be passed to functions and returned from functions just as numerical variables. If the loop function in the above line is called the next time, it returns "USD/JPY". And on all further calls it returns 0, as there are no further arguments.

asset(loop("EUR/USD","USD/JPY"))

The string returned by the loop function is now used as argument to the asset function. This function selects the traded asset, just as if it had been choosen with the [Asset] scrollbox. The string passed to asset must correspond to an asset subscribed with the broker. If asset is called with 0 instead of a string, it does nothing and returns 0. Otherwise it returns a nonzero value. This is used in the outer while loop:

while(asset(loop("EUR/USD","USD/JPY")))

This loop is repeated as long as the while argument, which is the return value of the asset function, is nonzero (a nonzero comparison result is equivalent to 'true'). And asset returns nonzero (= true) when the loop function returns nonzero. Thus, the while loop is repeated exactly two times, once with "EUR/USD" and once with "USD/JPY" as the selected asset. If Alice had wanted to trade the same strategy with more assets - for instance, also with commodities or stock indices - she only needed to add more arguments to the loop function, like this:

while(asset(loop("EUR/USD","USD/CHF","USD/JPY","AUD/USD","XAU/USD","USOil","SPX500","NAS100",...)))

and the rest of the code would remain the same. However, Alice does not only want to trade with several assets, she also wants to trade different algorithms. For this, nested inside the first while loop is another while loop:

while(algo(loop("TRND","CNTR")))

The algo function basically copies the argument string into the predefined Algo string variable that is used for identifying a trade algorithm in the performance statistics. It also returns 0 (= false) when it gets 0, so it can be used to control the second while loop, just as the asset function in the first while loop. Thus, for every repetition of the first loop, the second loop repeats 2 times, first with "TRND" and then with "CNTR". As these strings are now stored in the Algo variable, Alice uses them for calling the trade function in the inner code of the double loop:

if(Algo == "TRND")
  tradeTrend();
else if(Algo == "CNTR"))
  tradeCounterTrend();

The if condition checks if the Algo string is identical to a given string constant (== "TRND") and returns nonzero, i.e. true, in that case. So when Algo was set to "TRND" by the algo function, tradeTrend is called; otherwise tradeCounterTrend is called. Due to the two nested loops, this inner code is now run four times per run() call:

Those are the 4 asset/algorithm combinations - the components - of the strategy. If Alice had instead entered 10 assets and 5 algorithms in the loops, we had 50 (10*5) components and the inner code would be run fifty times every bar. Keep in mind that all parts of the script that are affected by the asset - for instance, retrieving prices or calling indicators - must be inside the asset loop. This is fulfilled here because anything asset related happens inside the two called trading functions.

Training and result

Now it's time to [Train] the strategy. Because we have now 4 components, 4x more bars, and twice as many cycles, the training process will take much longer than in the last workshop. As soon as it's finished, let's examine the resulting parameters. Use the SED editor to open the Workshop6_1.par file in the Data folder. This file contains the optimized parameters from the most recent WFO cycle. It could look like this:

EUR/USD:TRND +106 6.91 => 1.144
EUR/USD:CNTR 0.976 1.10 6.93 => 3.056
USD/JPY:TRND 386 6.93 => 3.089
USD/JPY:CNTR 1.73 1.09 5.75 => 1.899

As we can see, parameters for every asset/algo combination are optimized separately. Each line begins with the identifier for the component, consisting of asset and algorithm separated by a colon. Then follows the list of parameters. This allows Zorro to load the correct parameters at every asset() and algo() call. We can see that the tradeTrend() function uses two parameters and the tradeCounterTrend() function three, and their optimal values are slightly different for the "EUR/USD" and the "USD/JPY" assets. If a '+' sign appears in front of a parameter, its most robust value has been found at the start or end of its range. The last number behind the "=>" is not a parameter, but the result of the objective function of the final optimization run; it is for your information only and not used in the strategy.

Finally, let's have a look at the performance of this portfolio strategy. Click [Test], then click [Result].

You can see in the chart below that now two different algorithms trade simulaneously. The long green lines are from trend trading where positions are hold a relatively long time, the shorter lines are from counter-trend trading. The combined strategy does both at the same time. It generated about 80% annual profit with $400 capital requirement in the walk forward test. The equity curve contains all typical characteristics encountered with most profitable automated systems:

Portfolio analysis

Now look at the end of the performance report that popped up when clicking [Result] (it is stored under Workshop6_1.txt in the Log folder). You can see how the separate asset/algo combinations - the components - performed during the test:

Portfolio analysis  OptF  ProF  Win/Loss  Wgt%  Cycles


EUR/USD avg         .045  1.67  203/280   82.7  XXXXXXXXX
USD/JPY avg         .059  1.25  158/357   17.3  XXX\XXXXX


CNTR avg            .092  1.73  254/157   89.7  /XXXXXXXX
TRND avg            .034  1.15  107/480   10.3  XXXXXX\XX


EUR/USD:CNTR        .099  1.89  159/85    73.3  /X//X\XXX
EUR/USD:CNTR:L      .067  1.56   72/45    24.5  /\//\\/\X
EUR/USD:CNTR:S      .124  2.25   87/40    48.8  /////\\/\
EUR/USD:TRND        .037  1.23   44/195    9.4  XXXXX/\XX
EUR/USD:TRND:L      .000  0.89   19/96    -2.1  /\/\\/\\/
EUR/USD:TRND:S      .081  1.56   25/99    11.5  \/\///\/X
USD/JPY:CNTR        .094  1.40   95/72    16.4  /\X\/XXXX
USD/JPY:CNTR:L      .123  1.60   51/37    12.7  /\\\/////
USD/JPY:CNTR:S      .055  1.19   44/35     3.6  /\/\/\\\X
USD/JPY:TRND        .007  1.03   63/285    0.9  \/X\XX\X\
USD/JPY:TRND:L      .057  1.30   27/143    4.6  \/\\//\/\
USD/JPY:TRND:S      .000  0.74   36/142   -3.7  \//\\\\\\

The performance is listed for every component and separately for long (":L") and short (":S") trades (your own list might look different when you tested a different time period). We can see that the highest contribution to the profit (Wgt% column) came from the EUR/USD counter trend trading. Trend trading USD/JPY however did not perform as well and made only a small contribution. Different assets apparently often require different trade methods.

The performance report raises an obvious question. Wouldn't it be wise to invest more money in the more profitable components, and no money in the less profitable, such as USD/JPY trend trading? And how about reinvesting our profits? We can see in the performance report that the system produces about $1500 profit from about $400 required initial capital. How much more can we get, using the same strategy and the same capital, with a clever money management?

Four money management methods

Money management is an algorithmic method of distributing your capital among a portfolio of assets and trade algorithms in a way that your profit is maximized and your risk is minimized. These are contradictory conditions. Consequently, many different money management methods and philosophies exist. The simplest method, recommended in trade books, is: "Invest 1% of your account balance per trade". This is a bad advice: 1% is often either too low for a decent profit, or too high for an acceptable risk. In fact, all strategies will inevitably some day run into a margin call with the 1% method (read here why). The only question is how soon that happens.

Zorro follows a money management method developed by Ralph Vince. He published a computer algorithm that evaluates every component's balance curve for calculating the optimal percentage of the gained capital to be reinvested. This percentage is called the OptimalF factor (you can see it in the OptF column in the above performance sheet). Multiply OptimalF with the capital earned so far, and you'll get the maximum margin to be allocated to a certain trade. Normally, people try to stay well below this maximum margin. OptimalF is calculated from historical performance, thus there's no guarantee that this performance will continue in the future. Exceeding the maximum margin is far worse than staying below it, therefore hedge fund managers normally only invest about 50% of OptimalF.

Alice modified the run function for reinvesting profits (Workshop6_2):

function run()
{
  set(PARAMETERS+FACTORS); // use optimized parameters and reinvestment factors
  BarPeriod = 60;   // 1 hour bars
  LookBack = 2000;  // needed for Fisher()
  StartDate = 2005;  
  NumWFOCycles = 10; // activate WFO
  Capital = 10000;	  // initial capital

  if(ReTrain) {
    UpdateDays = -1;  
    SelectWFO = -1;	 
    reset(FACTORS); // don't re-train factors
  }
 
// portfolio loop
  while(asset(loop("EUR/USD","USD/JPY")))
  while(algo(loop("TRND","CNTR")))
  {
// set up the margin
    Margin = 0.5 * OptimalF * Capital * sqrt(1 + ProfitClosed/Capital);
 
    if(Algo == "TRND") 
      tradeTrend();
    else if(Algo == "CNTR") 
      tradeCounterTrend();
  }
}

There are three changes. At the begin of the script, Alice has now also set the FACTORS flag:

set(PARAMETERS+FACTORS);

One set() call can set several flags at the same time by just adding them with '+'. FACTORS lets Zorro calculate OptimalF factors. [Train] generates now not only parameters, but also factors separately for every strategy components. The factors are generated in the last optimization run and stored in Data\Workshop6_2.fac. It's a simple text file, so all factors can be examined and edited.

Re-training the strategy only uses the last WFO cycle; this is not suited for generating new OptimalF factors. Therefore Alice resets the FACTORS flag when the system is retrained:

if(ReTrain) {
  UpdateDays = -1;
  SelectWFO = -1;
  reset(FACTORS);
}

Alice has now also invested $10000 initial capital:

Capital = 10000;

We could see in the last performance report that the strategy without reinvestment needed a minimum capital of about $300. With $10000 we're certainly on the safe side. The OptimalF factors are used in the inner loop for determining the trade volume:

Margin = 0.5 OptimalF * Capital * sqrt(1 + ProfitClosed/Capital);

Margin is one of the methods for determining the amount invested per trade. Other methods were giving the number of lots or the risked amount. Margin is only a fixed percentage of the real trade volume - for instance 1% at 1:100 leverage - that the broker keeps as a deposit. If Margin is left at its default value, Zorro always buys 1 lot, the minimum allowed trade size. The higher the margin, the higher the number of lots and the higher the profit or loss. For determining the optimal margin for the strategy component, our initial Capital is multiplied with the 50% of the OptimalF factor. The result is then multiplied with a term that is supposed to start at 1 and to grow with the square root of the profit:

sqrt(1 + ProfitClosed/Capital)

ProfitClosed is the sum of the profits of all closed trades of the portfolio component. 1 + ProfitClosed/Capital is our capital growth factor. Let's examine the strange formula with an example. Assume we started with $10000 capital and the component made $20000 profit. The inner term inside the parentheses is then 1 + $20000/$10000 = 3. This is the growth of the capital: we started with $10000 and have now $3000 on our account, so it grew by factor 3. The square root of 3 is ~1.7, and this is the factor used for reinvesting our profits.

Why the square root - why don't we reinvest the whole profit? Because this would be very dangerous and can cause a margin call sooner or later. The reason is that the drawdown depth of almost all trade systems grows with time; details can be read here. This effect can be overcome by reinvesting only the square root of the profit. For checking how our reinvesting method affects the final return of the system, train it first for generating the OptimalF factors, then click [Test]:

Instead of the Annual Return, Zorro now displays the Compound Annual Growth Rate (CAGR). This is the average annual growth of the invested capital during the test period. For instance, with 30% annual growth our $10,000 initial capital would have grown to $13,000 at the end of the first year, to $16,900 at the end of the second year, and so on. Consequently the money management system now generates a multiple of the previous profit:

Now let's try the hedge fund manager method, and invest 50% of OptimalF - with no square root:

Margin = 0.5 * OptimalF * (Capital + ProfitClosed);

This method achieves indeed the highest profit, $130,000. But this comes at the cost of deep and dangerous drawdowns - most of the accumulated profit was lost in 2014. Trading this way requires strong nerves. It might also require having a packed suitcase ready for fleeing the country when your clients' money vaporized in a margin call.

Finally, let's try the amateur trader method, and invest 1% of the account balance as recommended in most trading books. Our margin is then calculated this way:

Margin = 0.01 * (Capital + ProfitClosed);

And this is the sad result:

Although the profit is much smaller than with any OptimalF investment method, the drawdowns are still significant. We can imagine that a slightly higher investment, like 2% or 3%, would bring us close to a margin call. In fact, if we continued to trade this way, even with only 1% it's mathematically certain that a drawdown will some day wipe out the account. By the way, the same would happen when you don't reinvest your profits, but regularly withdraw them - but then at least the withdrawn money is safe. Be always careful with reinvesting and withdrawing!
 

What have we learned in this workshop?

Next: Machine Learning


Further reading: ► while, asset, algo, loop, string, strstr