Template Class series<T>
In this section, we will take a closer look at class series<T>. In previous tutorials we have learned that incoming stream data is automatically organized into series collections. Examples of such built-in series collections are the open, high, low, close (among others) members of class in_stream.
Class series<T> is a container class that is similar to class std::vector<T> of the Standard Template Library (STL). The primary difference between these two container types is that with objects of type series<T> the most recently added value is always found at index position 0, that is, at the 'front' of the container and not at the 'back'.
Such series<T> objects store values contiguously and thus allow access to the underlying data via a pointer. This type of access improves performance for functions that need to repeatedly iterate over ranges of values.
Class series<T> is a template class! The template is however not entirely generic and its template parameter type is restricted to one of the following types:
•float •unsigned int •int •int64_t •double |
•bool •tsa::date_time •tsa::date •std::string |
In practice you will find that type 'double' will be used for the majority of your series declarations. In fact type 'double' is the default type, and you can use:
series<> as a synonym for series<double>
The majority of API functions declare parameters of type const series<double>& such as:
double true_range(const series<double>& hi, const series<double>& lo, const series<double>& cl);
double rate_of_change(const series<double>& series, size_t period);
double volatility(const series<double>& series, size_t period);
The same is true for the functions of the Domain Specific Language (DSL) extension that will be covered in the next chapter, where parameters and return values are often declared as series_cref<double>. A series_cref<double> object is simply a reference, or handle, to a series<double> object. The short form of series_cref<double> is type defined as sref<double> and is used heavily when writing DSL code. sref<double> is more concise and is used for better readability.
sref<double> CORRELATION(sref<double> series_a, sref<double> series_b, size_t period);
sref<double> RSI(sref<double> series, size_t period);
More on this in Chapter 3 on DSL.
Class series<T> as STL Style Container Class
Series objects have a size() and capacity() member as well as a reserve() member for setting capacity. The other difference between tsa::series<T> and std::vector<T> is that series<T> objects do NOT grow indefinitely. In fact, their capacity does not grow automatically, like it would for a std::vector<T> object.
When size hits capacity the series<T> object starts dropping the oldest values to make space for new ones. The reason why series<T> objects don't grow indefinitely is because from a strategy perspective it is neither required nor desired! For example, a 100 bar indicator function is only interested in the last 100 data items. Such a function won't be affected by older values being dropped.
This approach to memory management minimizes the application's memory footprint. Strategies thus only hold in memory the data they actually need at any one point in time. This allows strategies to iterate over decades of tick bars, or access data from numerous data streams simultaneously, without straining memory resources.
Lets take a look at Tutorial 115 which simply populates a series<int> object with a number sequence and prints out the series whenever a new value is added.
(1)
(2)
(3)
|
#include <assert.h> #include "tsa.h"
using namespace tsa;
void tutorial_115_basics(void) { series<int> s(10);
assert(s.capacity() == 10);
for (int n(10); n < 30; n++) { s.push(n); s.print(std::cout); std::cout << " size: " << s.size() << '\n';
if (s.size() > 3){ assert(s[0] == n); assert(s[1] == n-1); assert(s[2] == n-2); } assert(s.size() <= 10); // Size never grows larger than capacity!! // Old data is lost as new data is added. } } |
Program Output |
Tutorial: 115 ================================= [10] size: 1 [11,10] size: 2 [12,11,10] size: 3 [13,12,11,10] size: 4 [14,13,12,11,10] size: 5 [15,14,13,12,11,10] size: 6 [16,15,14,13,12,11,10] size: 7 [17,16,15,14,13,12,11,10] size: 8 [18,17,16,15,14,13,12,11,10] size: 9 [19,18,17,16,15,14,13,12,11,10] size: 10 [20,19,18,17,16,15,14,13,12,11] size: 10 <- series size now at capacity [21,20,19,18,17,16,15,14,13,12] size: 10 [22,21,20,19,18,17,16,15,14,13] size: 10 [23,22,21,20,19,18,17,16,15,14] size: 10 [24,23,22,21,20,19,18,17,16,15] size: 10 (... skipped some lines ...) press any key to exit... |
The above program output clearly shows that the latest numbers added are always at position 0 (left to right). Series size only grows until it hits capacity after which older values are pushed out on the right.
(1) |
series<int> s(10);
Creates a series object with capacity 10. |
(2) |
s.push(n); s.print(std::cout);
push() simply adds a given number to the front of the series. print() prints all values in the series to std::cout. |
(3) |
assert(s[0] == n); assert(s[1] == n-1); assert(s[2] == n-2);
Here we check that the number at position 0 equals the number most recently added, and that the number at position 1 is the previously added value and so forth. |
Populating a series from within a Strategy
The series we accessed in previous tutorials, such as:
•stream.open
•stream.high
•stream.low
•stream.close
are predefined series<double> members of class in_stream ( as well as of class instrument). Each series member object corresponds to a data field (or column) in the underlying data-source. The field names must be identical to the series names for the mapping to be successful.
You are however not limited to using just these built in series objects. You are free to define your own series objects as members of strategies. Such series can then be populated with whatever data is required. The library's Domain Specific Language (DSL) extension makes this very easy. Once declared as a member of a new strategy, such series can then be used in exactly the same way as built in series.
Tutorial 116 - Intercepting series_bounds_error exceptions
Tutorial 116 defines a strategy with a member of type series<double>. See (1) below. At every bar, we add a new value to this series. For demonstration purposes, we calculate a simple moving average and intercept the series_bounds_error exceptions that are thrown on the first few bars, as long as there exists an insufficient data condition. Series-bounds-errors were already discussed in tutorial 111.
(1)
(2)
(3)
(4)
|
#include "tsa.h"
using namespace tsa;
class my_strategy : public strategy { void on_start(void) override { std::cout << "Series capacity: " << s.capacity() << std::endl; }
series<double> s;
void on_next(void) override { double bc = (double)bar_count();
s.push(bc); // pushing new front value
std::cout << "date: " << (date)timestamp() << " series: "; s.print(std::cout); std::cout << '\n';
try{ double avg5 = average(s, 5); } catch (series_bounds_error e){ std::cout << "caught series-bounds-error" << std::endl; } } };
my_strategy s; s.run("2012-01-01", "2012-01-15");
|
Program Output |
Tutorial: 116 ================================= Series capacity: 3000 date: 2012-01-01 series: [1] caught series-bounds-error date: 2012-01-02 series: [2,1] caught series-bounds-error date: 2012-01-03 series: [3,2,1] caught series-bounds-error date: 2012-01-04 series: [4,3,2,1] caught series-bounds-error date: 2012-01-05 series: [5,4,3,2,1] date: 2012-01-06 series: [6,5,4,3,2,1] date: 2012-01-07 series: [7,6,5,4,3,2,1] date: 2012-01-08 series: [8,7,6,5,4,3,2,1] date: 2012-01-09 series: [9,8,7,6,5,4,3,2,1] date: 2012-01-10 series: [10,9,8,7,6,5,4,3,2,1] date: 2012-01-11 series: [11,10,9,8,7,6,5,4,3,2,1] date: 2012-01-12 series: [12,11,10,9,8,7,6,5,4,3,2,1] date: 2012-01-13 series: [13,12,11,10,9,8,7,6,5,4,3,2,1] date: 2012-01-14 series: [14,13,12,11,10,9,8,7,6,5,4,3,2,1] press any key to exit... |
(1) |
series<double> s;
Declares a series member object. |
(2) (3) |
double bc = (double)bar_count(); s.push(bc);
Adds the latest bar-count value to the (front) of the series. |
(4) |
try{ double avg5 = average(s, 5); } catch (series_bounds_error e){ std::cout << "caught series-bounds-error" << std::endl; }
Here we access our new series with the average() function. Normally we would instruct the strategy to handle any series-bounds-errors via a call to strategy::catch_series_bounds_errors(). However, for demonstration purposes, we explicitly intercept such exceptions here, and print a short message. As can be seen in the program output, the strategy threw four exceptions and then ran smooth on the fifth bar. |