Author Topic: Looping in AmiBroker AFL  (Read 28544 times)

administrator

  • Administrator
  • Full Member
  • *****
  • Posts: 200
    • View Profile
Looping in AmiBroker AFL
« on: August 23, 2010, 10:02:05 pm »
Author: gp_sydney
Source: http://finance.groups.yahoo.com/group/amibroker/files/

Introduction Rev 2 -June 2007

AmiBroker AFL uses arrays of numbers in very much the same way as it uses single
numbers. Arrays can be used in mathematical expressions, passed to functions, and
returned from functions, where the operations performed on the array are typically
equivalent to the same operation performed on every individual number in the array.
Much can be done without ever having to reference a particular number in an array.

However, more complicated operations and systems may require access to the
individual numbers in arrays, perhaps because there's no built-in function that
performs the required task, or because it's more efficient to implement it that way.
While AFL already provides a few functions that return individual values from arrays,
like LastValue, BeginValue, and EndValue, for much more than that you need to
access the elements yourself and use loops if you want to perform special functions on
all the elements of an array.

Arrays

Before you can really understand array indexing and looping, you need to understand
what an array is and how it's organised. Most programming languages support arrays,
with similar accessing methods, but AFL arrays have some limitations.

An AFL array can be thought of as a list of numbers, with one number in the list for
each bar on the current chart. What the numbers mean depends on the array. For
example, in the Close array shown above, each number is the closing price for the
corresponding bar, while in an ATR array it is the value of the ATR function at that
bar. The first number in the list (array) is for the first (oldest) bar on the chart, while
the last number in the list is for the most-recent bar on the chart. On days when the
stock didn't trade, and thus there's no bar, there won't be a number in the list. The
array index is the position of each number in the list relative to the start, with the first
number being at index zero.

Where AFL arrays differ from generic arrays in other programming languages is that
AFL arrays always match the bars on the current chart, so the size of the array (ie. the
number of values in the array) is the same as the number of bars on the chart. In other
programming languages it's usually possible to specify the array size yourself, and
store anything you like in the array.

administrator

  • Administrator
  • Full Member
  • *****
  • Posts: 200
    • View Profile
Re: Looping in AmiBroker AFL
« Reply #1 on: August 23, 2010, 10:04:50 pm »
Array Indexing

As mentioned above, an array index is just the position of a number in the array
relative to the start, with the first number being at index zero. Hence the last closing
price shown above of $9.35 is at array index eleven.

The syntax for referencing a number in an array in AFL, and in most other
programming languages, is with the opening and closing square bracket symbols '['
and ']' in the manner ArrayName[ArrayIndex]. So with the closing price array above:

Close[0] has the value $8.91
Close[1] has the value $8.86
Close[6] has the value $9.06
Close[11] has the value $9.35

Note that the values shown above are individual numbers, not other arrays. So while
in AFL you can write something like:

Avg = (High + Low) / 2;

and Avg will be an array covering all bars of the chart, you can also write something
like:

FirstAvg = (High[0] + Low[0]) / 2;

where FirstAvg is now a single number, not an array, as High[0] and Low[0] are also
single numbers.

Using Arrays And Numbers In Expressions

As arrays can be used in AFL in almost the same manner as individual numbers, it
can sometimes lead to confusion, particularly where the two are mixed in the same
expression. For example, the statement:

Avg = (High + Low[0]) / 2;

is completely different to either of the two examples above, yet is still valid AFL.
Now, however, each value in Avg (ie. the value at each bar) will be the average of the
High value at the same bar and the first value of Low at index zero.

So if the first three values of each array are (using a different example to above):

High: 1.10 -1.14 -1.20

Low: 1.00 -1.05 -1.18
Then the first three values of Avg would be:

Avg: 1.05 -1.07 -1.10
That is (1.10+1.00)/2, (1.14+1.00)/2, and (1.20+1.00)/2.
Variables can be even more confusing when assigned constant values. For example, in

the statement:

Avg = 0;

where Avg hasn't been used prior to this statement, Avg could be either an array or a
single number. As AFL doesn't require declaring what type Avg is in advance
(something that most other programming languages do require), whether Avg is an
array or single number can't be determined until later when it is actually used.

If and IIf

One place confusion between arrays and numbers commonly causes problems is with
the conditional If and IIf statements. If can only be used with single numbers, while
IIf is used with arrays. So while we can write:

if (Close[3] > Open[3])

since Close[3] and Open[3] are single numbers, we can't write:

if (Close > Open)

since Close and Open are arrays of numbers. To achieve this, perhaps to generate an
array that indicates which days have higher closes than opens, we would have to write
something like:

HighClose = IIf(Close > Open, True, False);

The array HighClose would then have a True (value of one) for every bar where the
close was higher than the open, and a False (value of zero) for every bar where the
close was lower than or equal to the open. Note though that this statement could be
more simply written as just:

HighClose = Close > Open;

since the result of any comparison is always True (one) or False (zero), ignoring null
values for now. Likewise for individual numbers in the arrays:

HighClose[10] = Close[10] > Open[10];

However, if the resulting array is assigned any value other than True or False, then the
IIf statement is required:

HighOpenClose = IIf(Close > Open, Close, Open);

In this example, each value in HighOpenClose is the higher of the open or closing
prices at each bar. This is equivalent to writing for each bar:

if (Close[0] > Open[0])

HighOpenClose[0] = Close[0];

else

HighOpenClose[0] = Open[0];

if (Close[1] > Open[1])

HighOpenClose[1] = Close[1];

else

HighOpenClose[1] = Open[1];

etc.

Naturally this could be written more readily using a loop, but we'll get to that.

Null Values In Arrays

The null value is used to indicate that no data exists for a bar in an array. While this
wouldn't normally occur in price arrays like Open and Close, it does in things like
moving average arrays where no value is defined for the first "period" bars. So if a
five day exponential moving average is obtained:

e5 = EMA(Close, 5);

then the first five numbers in array e5 are all null (the number -1e10).
When plotted on a chart, no data will be displayed for these bars. To simplify array
mathematics, any expression involving a null value will give a null result, so:

Null + 3 = Null
SQRT(Null) = Null
10 > Null = Null

The comparison example above is particularly noteworthy, as normally comparisons
result in either True (one) or False (zero). This property of Null means that it's not
possible to test for Null directly in an If statement:

if (e5[4] == Null) // This won't work!

As e5[4] == Null is Null, this is the same as if (Null) which is never true (despite the
fact that the null value -1e10 is non-zero). To test for Null, the IsNull() function can
be used:

if (IsNull(e5[4]))

Note that IsNull() will return an array if passed an array, in which case IIf would be
required rather than If:

e5x = IIf(IsNull(e5), Close, e5);

In this example, numbers in e5x have the same value as in e5 provided they are not
null, otherwise they have the same value as in Close.

And as indicated earlier, if the assigned values are just True and False, then IIf is not
required:

emaNull = IsNull(e5);

In this example, emaNull is an array that will have a value of True (one) for every bar
where the equivalent bar in e5 is null, or a value of False (zero) otherwise.

administrator

  • Administrator
  • Full Member
  • *****
  • Posts: 200
    • View Profile
Re: Looping in AmiBroker AFL
« Reply #2 on: August 23, 2010, 10:09:44 pm »
Looping

At last we are ready to look at looping in AFL. Looping is typically used where the
same operations need to be performed on multiple numbers in an array using array
indexing to access the individual numbers in the array. If the operations are the same
as a standard function already built into AmiBroker, then it's easier to use the built-in
function. For example, to take the square root of all numbers in an array, the built-in
SQRT function can just be used on the array:

array2 = SQRT(array1);

However, if the operations are not the same as a built-in function, or different
operations are required on different numbers in the array, then looping may be
required. In some cases it may be possible to achieve the same result using multiple
built-in array functions, but looping may be more efficient.

There are three constructs available for looping:

for ( ; ; )
{
    ....
}

while ( )
{
    ....
}

do
{
    ....
}

while ( );

The last two ("while" and "do") are almost identical, and "for" is probably the most
commonly used. These constructs are essentially the same as in the C and C++
programming languages, and as in those languages, the placement of the braces is
arbitrary and a matter of personal preference. Also, if the code in the loop is only a
single line, then the braces aren't required at all.

For clarity of reading, it is standard practice to indent code inside a loop or an If
statement (typically four spaces or one tab -although I personally prefer spaces to
hard tabs). This makes it easier to ensure each opening brace has a matching closing
brace, something that is critical to avoid language runtime errors (or compiler errors
in C and C++). For placement of braces, one favoured method is as shown above,
which I will continue to use in this document. Another, which I actually prefer and
normally use myself, is:

for ( ; ; ) {
    ....
}

As long as each opening brace has a matching closing brace, it doesn't really matter
where you put them. The language treats spaces, tabs, and newlines the same, all as
white space (the main exception being where braces aren't required if only one line of
code exists inside the loop). Where the language requires white space between other
characters or symbols, or possibly no white space at all, any number of spaces, tabs,
and newlines can generally be used. So:

x=3;

is the same as:

x = 3;

which is the same as:

x = 3;

Before starting to look at the three loop constructs, one final thing needs to be
mentioned in relation to array indexing. In all the earlier examples, the array indices
were constant values: Close[0], Open[10], and e5[6]. This is not generally very useful
with looping, as the loop typically wants to access all the values in an array and there
could be thousands of them. With loops, the array index is more commonly a variable
that changes with each pass of the loop (don't worry about the while construct yet):

Code: [Select]
i = 0; // 'i' is an index variable
while (i < BarCount) // 'i' used in loop termination condition
{
    Avg[i] = (High[i] + Low[i]) / 2; // 'i' used to index into arrays in loop
    i++; // 'i' incremented at end of each loop
}

In this example, 'i' is the loop index variable. The reason 'i' is typically used as the
first loop variable comes from the days of Fortran, where variables starting with 'i' and
a number of letters after that were integer values (ie. whole numbers), while variables
starting with other letters were floating point (fractional) values. Integers are best for
loop index variables, as floating point values are subject to rounding errors which can
prevent loop termination conditions from being met. Despite that, all numbers in AFL
are floating point, so care needs to be taken with mathematical operations on loop
index variables to ensure rounding errors don't prevent loop termination. Simple
operations like addition and subtraction rarely cause rounding errors, but other more
complex operations (like division) can.

The variable BarCount is a built-in variable in AmiBroker equal to the total number of
bars in the current chart.

The construct i++ is called post-incrementing and is detailed in the AmiBroker help. It
is essentially shorthand for i = i + 1. While incrementing the loop index by one is by
far the most common scenario, any integer operation on the index could be used, for
example, i = i + 30.

For Loops
Code: [Select]
Avg = Null; // Fill result array with null values
for (i = 10; i < BarCount; i++) // Run loop from 10 to BarCount-1
    Avg[i] = (High[i] + Low[i]) / 2; // Calculate average at each bar

Note that braces are not required here because there is only one line of code inside the
for loop, but the line is still indented for clarity. Also note that the first initialisation
statement Avg = Null sets all array values to null, as Avg is an array.

Another for loop example that performs a similar operation to the ExRem function on
the Buy and Sell arrays:

Code: [Select]
OpenPos = False; // No open position to start with
for (i = 0; i < BarCount; i++) // Loop over all bars in the chart
{
    if (OpenPos) // If have an open position
   {
        Buy[i] = False; // Remove any surplus buy signals
        if (Sell[i]) // If have sell signal on this bar
       OpenPos = False; // No longer have open position
    }
    else // Else if don't have open position
    {
       Sell[i] = False; // Remove any surplus sell signals
       if (Buy[i]) // If have a buy signal on this bar
       OpenPos = True; // Now have an open position
    }
}

In this example, the variable OpenPos is known as a state variable, meaning it
maintains the state of whether we currently have an open position in the stock or not.
It is a single number, not an array, and in this example only ever holds the values True
(one) or False (zero). While we do have an open position, we aren't interested in any
more buy signals. Similarly, while we don't have an open position, we're not
interested in any sell signals.
« Last Edit: August 23, 2010, 10:11:46 pm by administrator »

administrator

  • Administrator
  • Full Member
  • *****
  • Posts: 200
    • View Profile
Re: Looping in AmiBroker AFL
« Reply #3 on: August 23, 2010, 10:15:13 pm »
The statement if (OpenPos), where it's not compared to anything, just means if
OpenPos is non-zero. The If function only ever tests for a zero or non-zero
expression, where all comparison operations like i > 3 and i < BarCount are non-zero
(one) if they are true and zero if they are false. So the statement if (OpenPos) is
equivalent to if (OpenPos == True), although it would also be true if OpenPos was
any other non-zero value (ie. not just one). For example:

Code: [Select]
e1 = EMA(Close, 30);
e2 = EMA(Close, 60);
ediff = e1 -e2;
for (i = 0; i < BarCount; i++)
{
  if (ediff[i])
  {
    ....
  }
  else
  {
    ....
  }
}

While Loops

// Difference between e1 and e2
// If the difference is any non-zero value
// Else if the difference is zero

Only one expression is required for a while loop, resulting in a value that must be
non-zero (typically one) for the loop to continue. While perhaps not used as often as
for loops, while loops are probably the simplest and easiest to understand. Rewriting
our common for loop as a while loop, we get:

Code: [Select]
i=0; // Initialise loop index
while (i < BarCount) // Loop continuation condition
{
  ....
  i++; // Increment loop index
}

In fact, any for loop can be rewritten this way, putting the initial condition as a
separate statement before the while loop, including the continuation condition in the
while statement, and putting the index change function as the last operation before the
end of the loop (although use of the Break and Continue control words can upset this
comparison). So rewriting our simple averaging function as a while loop would give:

Code: [Select]
Avg = Null;
i = 10;
while (i < BarCount)
{
  Avg[i] = (High[i] + Low[i]) / 2;
  i++;
}

// Fill result array with null values
// Initialise loop index to 10
// Loop until index reaches BarCount

// Calculate average at each bar
// Increment loop index

Note that as we now have two lines of code in the while loop, the braces become
necessary.

And rewriting our ExRem equivalent using a while loop:

Code: [Select]
OpenPos = False;
i = 0;
while (i < BarCount)
{
  if (OpenPos)
  {
    Buy[i] = False;
    if (Sell[i])
      OpenPos = False;
  }
  else
  {
    Sell[i] = False;
    if (Buy[i])
      OpenPos = True;
  }
  i++;
}

administrator

  • Administrator
  • Full Member
  • *****
  • Posts: 200
    • View Profile
Re: Looping in AmiBroker AFL
« Reply #4 on: August 23, 2010, 10:18:31 pm »
Do Loops

// No open position to start with
// Initialise loop index
// Loop over all bars in the chart

// If have an open position

// Remove any surplus buy signals
// If have sell signal on this bar
// No longer have open position

// Else if don't have open position

// Remove any surplus sell signals
// If have a buy signal on this bar
// Now have an open position

// Increment loop index

A do loop is a slight variation on a while loop, but much less commonly used. The
main practical difference is that a while loop may not be executed at all if the
continuation condition is false at the start, while a do loop will always be executed at
least once, since the continuation condition is at the end of the loop rather than at the
start.

Our common for loop now as a do loop:

Code: [Select]
i=0;
do
{
  ....
  i++;
}
while (i < BarCount);

// Initialise loop index
// Start loop -no condition specified yet

// Increment loop index
// Test continuation condition

The only difference to a while loop is that the initial while statement has been
replaced with the word "do" and moved to the end of the loop instead. Since the
continuation condition is not tested until the end of the loop, the loop will always
execute at least once, even if the continuation condition is false at the start.

So in the following situations, if Close[0] equals $10.00:

Code: [Select]
i = 0;
while (Close[i] <= 5)
{
  ....
  i++;
}

and:

Code: [Select]
i = 0;
do
{
  ....
  i++;
}
while (Close[i] <= 5);

the while loop won't execute at all because it detects Close being greater than $5.00
before starting the loop, but the do loop will execute once because it doesn't detect
that condition until the end of the first pass. Note that "<=" means less than or equal
to.

Other than the difference outlined above, do loops are identical to while loops. For
completion though, our ExRem equivalent rewritten with a do loop:

Code: [Select]
OpenPos = False; // No open position to start with
i = 0; // Initialise loop index
do // Start loop
{
  if (OpenPos) // If have an open position
  {
    Buy[i] = False; // Remove any surplus buy signals
    if (Sell[i]) // If have sell signal on this bar
      OpenPos = False; // No longer have open position
  }
  else // Else if don't have open position
  {
    Sell[i] = False; // Remove any surplus sell signals
    if (Buy[i]) // If have a buy signal on this bar
      OpenPos = True; // Now have an open position
  }
  i++; // Increment loop index
}
while (i < BarCount); // Loop over all bars in the chart