Preparing DSL
In this section we will take a look at some practical DSL examples. Tutorial 200 presents a simple strategy that produces three moving averages - 'simple', 'double-smoothed' and 'triple-smoothed'. As explained in the DSL introduction, all DLS code must be called by the strategy's on_prepare_dsl() member which translates the DLS code into internally managed object structures that actual do the heavy lifting when strategy::run() is called. Much of the underlying complexity is thus hidden from developers.
Tutorial 200:
(1)
(2)
(3)
(4)
(5)
|
#include "tsa.h" #include "tsa-graphics.h"
using namespace tsa;
class my_strategy : public strategy { in_stream in; chart ch;
void on_start(void) override { in.as_random_walk(); }
series<double> single_smoothed, double_smoothed, triple_smoothed;
void on_prepare_dsl(void) override { single_smoothed = SMA(in.close, 5); double_smoothed = SMA(SMA(in.close, 5), 5); triple_smoothed = SMA(SMA(SMA(in.close, 5), 5), 5); }
void on_bar_close(void) override { if (first()){ std::cout << "on_next() invoked first on bar: " << bar_count() << std::endl; }
ch << chart::pane(500) << plot::ohlc(in, color::gold) << "random-walk" << single_smoothed << "smoothed" << double_smoothed << "double-smoothed" << triple_smoothed << "tripple-smoothed"; } };
my_strategy s;
s.name("tutorial_200"); s.output_base_path(os::path("output")); s.enable_reports(); s.auto_open_reports();
s.run("2010-02-01", "2012-03-01"); |
|
Program Output:
Tutorial: 200 ================================= on_next() invoked first on bar: 15 press any key to exit...
|
(1) |
series<double> single_smoothed, double_smoothed, triple_smoothed;
Declares three series members. These series objects will hold the newly calculated moving average data. The reason why the series declarations are not declared inside the definition of the on_prepare_dsl() member is twofold: 1.We will need to access and use these moving averages series in other parts of our strategy; such as in strategy::on_bar_close(). 2.Declaring series objects inside strategy::on_prepare_dsl() is not permitted (doing so would throw an exception at run-time) as any local series objects would go out of scope when on_prepare_dsl() exits. This would break the internal bindings that are put in place during DSL translation. This is why DLS functions only return references to series, usually as type sref<T>. |
|
(2)
|
void on_prepare_dsl(void) override { single_smoothed = SMA(in.close, 5); double_smoothed = SMA(SMA(in.close, 5), 5); triple_smoothed = SMA(SMA(SMA(in.close, 5), 5), 5); }
Here we define our DSL code. Recall that semantically, DSL functions do not return just a single value, but an entire series. This is why we use the assignment operator and not the series<T>::push() member.
To avoid redundancy, the function could have been written as:
void on_prepare_dsl(void) override { single_smoothed = SMA(in.close, 5); double_smoothed = SMA(single_smoothed, 5); triple_smoothed = SMA(double_smoothed, 5); } |
|
(3) |
void on_bar_close(void) override { if (first()){ std::cout << "on_next() invoked first on bar: " << bar_count() << std::endl; }
One of the benefits of using DSL is that we don't need to worry about series-bounds-error exceptions. The internal DSL logic automatically checks the length of series arguments before evaluating function logic. If series data is insufficient in length, the function won't be evaluated. This is also a lot faster than handling series_bounds_error exceptions since the stack unwinding that occurs after an exception is expensive.
Here we are interested in the number of bars that were skipped by the internal DSL mechanism before on_bar_close() was called for the first time.
The above code will print the ordinal number of the bar on which on_bar_close() was first called. This will show us just how many bars were skipped while working through the DSL code. The program output is:
Tutorial: 200 ================================= on_next() invoked first on bar: 15 press any key to exit...
It took 15 bars until the DSL code had sufficient data to evaluate successfully. This was expected, since the periods given in:
SMA(SMA(SMA(in.close, 5), 5), 5);
... add up to 15.
Using DSL code is thus quite painless compared to the alternative we saw in previous sections! It combines the feel of a dedicated timeseries language with the full capabilities of C++. |