Did you notice that the stock data in the CSV files are ordered from most recent to oldest? This means that when we read it into arrays from top to bottom, the first element will correspond to the most recent stock price. Let’s reverse the arrays so they’re oriented in a more natural way, going forward in time with the index number. If we express the reverse operation as a function, we could apply it to any array like this:
adjclose = reverse(adjclose)
The reverse function will prove useful for the other two objectives of the stock-prices app. Before implementing it, we need to understand how array indexing and slicing works.
To select a single element, we enclose an integer index inside the parentheses; for example adjclose(1) will refer to the first element of the array, adjclose(5) to the fifth, and so on.
To select a range of elements–for example, from fifth to tenth–use the start and end indices inside the parentheses, separated by a colon:
real, allocatable :: subset(:)
...
subset = adjclose(5:10)
In this case, subset will be automatically allocated as an array with six elements, and values corresponding to those of adjclose from index 5 to 10.
By default, the slice adjclose(start:end) will include all elements between the indices start and end, inclusive. However, you can specify an arbitrary stride. For example, adjclose(5:10:2) will result in a slice with elements 5, 7, and 9. The general syntax for slicing an array a with a custom stride is
a(start:end:stride)
where start, end, and stride are integer variables, constants, or expressions. start and end can have any valid integer value, including zero and negative values. stride must be a nonzero (positive or negative) integer.
Similar rules apply for start, end, and stride as apply for do loops:
- If
strideis not given, its default value is 1. - If
start>endandstride>0, or ifstart<endandstride<0, the slice is an empty array. - If
start==end–for example,a(5:5)–the slice is an array with a single element. Be careful not to mistake this fora(5), which is a scalar (nonarray).
Furthermore, if start equals the lower bound of an array, it can be omitted, and the same is true if end equals the upper bound of an array. For example, if we declare an array as real :: a(10:20), then the following array references and slices all correspond to the same array: a, a(:), a(10:20), a(10:), a(:20), a(::1). The last syntax from this list is particularly useful when you need to slice every n-th element of the whole array–it’s as simple as a(::n). If you have experience with slicing lists in Python, this will feel familiar.
Exercise 2: Reversing an array
Write a function reverse that accepts a real one-dimensional array as an input argument and returns the same array in reverse order. Use array slicing rules to perform the reversal. You can test your new function by reversing the input array twice and comparing it to itself:
print *, all(a == reverse(reverse(a)))
Here, all is the built-in function that takes a logical array as input and returns .true. if all elements evaluate as .true..
Hint: use the built-in function size to determine the end index of the input array.
You can find the solution in the “Answer key” section near the end of this chapter.
Play with different ways to slice arrays. Try different values of start, end, and stride. What happens if you try to create a slice that’s bigger than the array itself? In other words, can you reference an array out of bounds?
Referencing array elements out of bounds
Be very careful to not reference array elements that are out of bounds! Fortran itself doesn’t forbid this, but you’ll end up with either an invalid value or a segmentation fault, which can be particularly difficult to debug.
By default, compilers don’t check if an out-of-bounds reference occurs during runtime, but you can enable it with a compiler flag. Use gfortran -fcheck=bounds and ifort -check bounds for GNU and Intel Fortran compilers, respectively. This can result in significantly slower programs, so it’s best if used only during development and debugging.
Now that we understand how array indexing works, it’s straightforward to calculate the stock gain over the whole time series. Take the difference between the last and first element of the adjusted close price to calculate the absolute gain in US dollars:
adjclose = reverse(adjclose) ❶
gain = (adjclose(size(adjclose)) - adjclose(1)) ❷
❶ Reverses the array so that the first element refers to the oldest record
❷ Takes the difference between the last and the first element
Here, I’m using the built-in size function, which returns the integer total number of elements, to reference the last element of the array. Like everything else we did before, gain must be declared, in this case as a real scalar. The absolute gain, however, only tells us how much the stock grew over a period of time; it doesn’t tell us anything about whether that growth is small or large relative to the stock price itself. For example, a gain from $1 to $10 per share is greater than a gain from $100 to $200 per share, assuming you invest $100 in either stock. In the former case, you’ll come out with $1,000, whereas in the latter case, you’ll have just $200! To calculate the relative gain in percent, we can divide the absolute gain by the initial stock price, and multiply by 100 to get the percent; that is, gain / adjclose(1) * 100. For brevity, I’ll also round the relative gain to the nearest integer using the built-in function nint:
print *, symbols(n), gain, nint(gain / adjclose(1) * 100)
2000-01-03 through 2018-05-14
Symbol, Gain (USD), Relative gain (%)
-------------------------------------
AAPL 184.594589 5192
AMZN 1512.16003 1692
CRAY 9.60000038 56
CSCO 1.71649933 4
HPQ 1.55270004 7
IBM 60.9193039 73
INTC 25.8368015 89
MSFT 59.4120979 154
NVDA 251.745300 6964
ORCL 20.3501987 77
From this output, we can see that Amazon had the largest absolute gain of $1,512.16 per share, and Hewlett-Packard had the smallest gain of only $1.55 per share. However, the relative gain is more meaningful than the absolute amount per share because it tells how much the stock has gained relative to its starting price. Looking at relative gain, Nvidia had a formidable 6,964% growth, with Apple being the runner up with 5,192%. The worst performing stock was that of Cisco Systems (CSCO), with only 4% growth over this time period.
If you’ve cloned the stock-prices repo from GitHub, it’s straightforward to compile and run this program. From the stock-prices directory, type
make
./stock_gain
We’ve now covered a lot of the nitty-gritty of how arrays work. Let’s apply this knowledge to the other two challenges we have for the main exercise for this chapter.