Lambda Functions in DSL
There are two reasons why c++11 lambda functions would be used to extend DSL.
•To perform an operation that DSL can't otherwise do
•Integrate in an existing c or c++ function
•Avoid DSL overhead
DSL features a few functions that make it easy to drop in a lambda. These are:
TRANSFORM<T>(sref<T>) |
This functions allows you to drop in a lambda that takes a value, and returns a derived value. |
TRANSFORM_RANGE<T>(sref<T>,size_t) |
This function allows you to drop in a lambda that calculates a derived value from a range of values - such as for calculating a moving average. |
TRANSFORM_TYPE<T0,T1>(sref<T0>,sref<T1>) |
This function allows you to use a lambda that takes an argument of one type, and returns a value of another type. The example below take turns a series of weekday-id's (int's) into weekday-names (strings) |
A Simple Transform
Say you have a series of numeric values and you want to perform a simple calculation, like return the absolute value of the number.
From Tutorial 203:
sref<double> ABS(sref<double> _data)
{
return TRANSFORM<double>(_data,
[](double value){return std::fabs(value);});
}
The first argument to TRANSFORM is the data series itself. The second is a lambda function that declares a single parameter and returns a single value. When strategies run, series grow one value at a time. What we are doing here is submit the logic that needs to be performed at the moment when a series is given a new value during strategy evaluation.
A Simple Transform from Range Data
The fastest DSL functions are the ones that are implemented via a functor (function object). Functors are stateful and thus allow for optimized algorithm. See the section on DSL Functors on how to do this. But implementing a functor can sometimes be a lot of work. Sometimes you just wand to drop in some simple C++ that can't easily be expressed in DSL, or you may be looking to integrate a function from an existing C++ library. This example simply calculates a moving average over a given range. From Tutorial 203:
sref<double> AVERAGE(sref<double> _data, size_t _period)
{
verify_non_zero(_period);
return TRANSFORM_RANGE<double>(_data, _period,
[](const double* data, size_t period)
{
double sum = 0.0;
for (size_t c(0); c < period; c++) {
sum += data[c];
}
return sum / (double)period;
});
}
This function simply calls the TRANSFORM_RANGE<T> function. We pass the series reference as first argument and the period as second argument. The period is the size of the range that we are allowed to look at, in other words, the size of the range that is guaranteed to be available when the lambda is called. The lambda is passed as third argument. The lambda itself declared two parameters. The first is a data pointer, and the second is the size of the available range. Do not make mistakes here as the DLS mechanism cannot protect you from memory errors here!
Transforming Type
What if the lambda function wants to return a type different from the parameter type? The following function takes as argument series of integers representing the week-days and returns a series of strings containing the weekday names. From Tutorial 203:
sref<std::string> WEEKDAY_NUM_TO_NAME(sref<int> weekday_num)
{
return TRANSFORM_TYPE<std::string, int>(weekday_num,
[](int num) {
switch (num) {
// as per tsa::date::week_day
case 0: return "Sunday"; break;
case 1: return "Monday"; break;
case 2: return "Tuesday"; break;
case 3: return "Wednesday"; break;
case 4: return "Thursday"; break;
case 5: return "Friday"; break;
case 6: return "Saturday"; break;
default: return "not a weekday";
}
});
}
Although DSL operates on series, the lambda again works on just one value at a time.
Handling Different Parameter Sets
What if you want to plug in lambda to take three series and five scalars as argument? There is no generic solution to this yet, but all the TRANSFORM functions are template functions. You can just make copies and manually edit them to accommodate your needs. You can also request support from the library publishers.
Optimizing DSL
Another reason for using lambdas may be to optimize DSL. DSL code gets translated into a large tree bound function objects. If your DSL primarily calls DSL functions representing optimized functors, then your code will be very fast. However, if your DSL consists of long lines of small arithmetic operations involving constants, then this can cause substantial overhead. For example, many financial indicators, such as the Relative Strength Indicator (RSI) normalize the output with an expression similar to:
rv = 100 - (100 / (1 + rs));
This is an obvious candidate for a lambda since it has three operators as well as three constants, which would cause this expression to translate into a number of internal function objects. The following code uses the TRANSFORM() function and corresponding lambda functions to avoid much of this overhead. Note that the code also replaced the IF() function with lambdas.
series_cref<double> RSI(series_cref<double> ser, size_t period) {
auto chgs = ser - SHIFT(ser, 1);
auto up_chg = TRANSFORM(chgs, [](double d) {return d > 0.0 ? d : 0.0; });
auto dn_chg = TRANSFORM(chgs, [](double d) {return d < 0.0 ? std::fabs(d) : 0.0; });
auto avg_up = EMA_WILDER(up_chg, period);
auto avg_dn = EMA_WILDER(dn_chg, period);
auto rs = SAFE_DIVIDE(avg_up, avg_dn);
auto rv = TRANSFORM(rs, [](double num) {return 100.0 - (100.0 / (1.0 + num)); });
return rv;
}