Lua Extension Modules

From FxCodeBaseWiki
Jump to: navigation, search

What Is Lua Extension Modules?

The Lua Extension Module is a dynamic link library which contains the set of functions written in C or C++ which can be used from Lua code.

The article supposes that you are familiar with Lua VM and developing using Lua VM. You can read about Lua VM in Lua documentation, Section 3. The Application Program Interface.

Please note that Lua Extension Module can be developed only in C or C++. This article supposes that you use Microsoft Visual Studio, but any Win32 C++ compiler may be used instead.

How To Start?

Preparing Environment

All you need is download and install the latest version of the Indicore Integration SDK. The latest version is always available here: IndicoreDev.exe

Creating the Project

  • Create a new C++/Win32 project.
  • Choose DLL application type and create an empty project
  • In the linker options, "Input" section add C:\Gehtsoft\IndicoreDev\lib\lua5.1.lib and C:\Gehtsoft\IndicoreDev\lib\indicore2.lib to Additional Dependencies property.
  • The name of the output file must be exactly the same as the name of the extension table you are going to use. In our case we are going to implement the geometric mean value function for Lua, so let's output module is called geomean.dll.

Header files

Add the following headers into your source file:

#include <windows.h>
#include <stdio.h>
#include <math.h>
#include <tchar.h>
#include <string>
#include <vector>
#include <map>
#include <list>
#define INDICORE2_EXTKIT
#include "C:/Gehtsoft/IndicoreDev/include/indicore2.h"

Write the Extension Function Template

Then you must create an extension function template. All the extension function has exactly the same prototype. You can use any name but the good style is to use the name which is similar to the name we want to use.

/** The geomean.calc(stream, from, to). 

    stream the tick stream
    from   the first period of the range to calculate
    to     the last period of the range to calculate

    The function return the geometric mean value (1 number)
  */
int geomean_calc(lua_State *L)
{
    double ret = 0;
    //TBD: Calculate mean value into ret variable
  
    lua_pushnumber(L, ret);
    return 1;
}

Create Module Description

Now we must create a description of the extension module table. The extension module table is an array of luaL_reg structures. Each element of the array corresponds to one function and defines the name of the function as it is visible in lua. The last element must be a structure filled by zeros.

static const struct luaL_reg geomean_lib [] =
{
     {"calc", geomean_calc},
     {NULL, NULL}
};

Create the Module Registration Function

Now we must create an exportable function which will be called when Lua is requested to load our extension module. The name of this function must be created using the following pattern: luaopen_name where name is the name of the extension module (geomean in our case).

extern "C" int __declspec(dllexport) luaopen_extension (lua_State *L)
{
    luaL_register(L, "geomean", geomean_lib);

    lua_getglobal(L, "geomean");
    if (lua_isnil(L, -1) || !lua_istable(L, -1)) {
        luaL_error(L, "the geomean library is not registered");
    }
    return 1;
}

Preparing the Test Indicator

The indicator is very similar to a simple moving average (MVA.lua) indicator. It just takes one integer parameter N (a number of periods), can be applied on the tick data and can be calculate starting at Nth bar. Let's create the indicator skeleton. For for information about writing indicator please refer the Indicore SDK, especially the step by step instruction on creating a simple indicator.

function Init()
    indicator:name("Test indicator");
    indicator:description("");
    indicator:requiredSource(core.Tick);
    indicator:type(core.Indicator);

    indicator.parameters:addGroup("Calculation");
    indicator.parameters:addInteger("N", "Number of periods", "", 10, 2, 1000);
    indicator.parameters:addGroup("Style");
    indicator.parameters:addColor("C", "Line Color", "", core.rgb(0, 0, 255));
    indicator.parameters:addInteger("W", "Line Width", "", 1, 1, 5);
    indicator.parameters:addInteger("S", "Line Style", "", core.LINE_SOLID);
    indicator.parameters:setFlag("S", core.FLAG_LEVEL_STYLE);
end

local n;
local first = nil;
local source = nil;
local out = nil;

require("geomean");

function Prepare(onlyName)
    local name;

    name = profile:id() .. "(" .. instance.source:name() .. ")";
    instance:name(name);
    if onlyName then
        return ;
    end

    source = instance.source;

    n = instance.parameters.N;
    first = source:first() + n - 1;
    out = instance:addStream("GEOMEAN", core.Line, name, "GEOMEAN", instance.parameters.C, first);
    out:setWidth(instance.parameters.W);
    out:setStyle(instance.parameters.S);
end

function Update(period, mode)
    if period >= first then
        out[period] = geomean.calc(source, period - n + 1, period);
    end
end

Pay attention on the highlight lines. The first forces Lua to load our extension. The second is a call of our function.

Implementing Extension Function

Now we can implement the extension function. First, we must get the parameters from the stack. As you see from the indicator code, the first parameter is a stream. So, we need to know how to convert the Lua table into IIndicatorTickSource interface. This is why Indicore Integration SDK is required in this particular project. It has IndicoreAccessors class designed for that purpose.

So, get the parameter values from the stack and check the types.

/** The geomean.calc(stream, from, to). 

    stream the tick stream
    from   the first period of the range to calculate
    to     the last period of the range to calculate

    The function return the geometric mean value (1 number)
  */
int geomean_calc(lua_State *L)
{
    double ret = 0;

    IIndicatorTickSource *src = IndicoreAccessors::getIIndicatorTickSource(L, 1);
    if (src == 0)
        return luaL_error(L, "the first parameter must be source.");

    if (lua_isnumber(L, 2) == 0)
        return luaL_error(L, "the second parameter must be number");
    int from = (int)lua_tonumber(L, 2);

    if (lua_isnumber(L, 3) == 0)
        return luaL_error(L, "the third parameter must be number");
    int to = (int)lua_tonumber(L, 3);
  
    lua_pushnumber(L, ret);
    return 1;
}

Now, check the from and to parameters. Both of them must be in the range of the price source data and to must be after from.

/** The geomean.calc(stream, from, to). 

    stream the tick stream
    from   the first period of the range to calculate
    to     the last period of the range to calculate

    The function return the geometric mean value (1 number)
  */
int geomean_calc(lua_State *L)
{
    double ret = 0;

    IIndicatorTickSource *src = IndicoreAccessors::getIIndicatorTickSource(L, 1);
    if (src == 0)
        return luaL_error(L, "the first parameter must be source.");

    if (lua_isnumber(L, 2) == 0)
        return luaL_error(L, "the second parameter must be number");
    int from = (int)lua_tonumber(L, 2);

    if (lua_isnumber(L, 3) == 0)
        return luaL_error(L, "the third parameter must be number");
    int to = (int)lua_tonumber(L, 3);

    if (from < src->getFirstPeriod() || from >= src->size() ||
        to < src->getFirstPeriod() || to >= src->size())
        return luaL_error(L, "the period index is out of range");

    if (to <= from)
        return luaL_error(L, "the third parameter must be greater than the second");

  
    lua_pushnumber(L, ret);
    return 1;
}

Now implement the geometric mean calculation. The formula of the geometric mean of n value is:

<math>\operatorname{geomean}(price, n)_i = \sqrt[n]{\prod_{j=i-n+1}^i price_j} = ({\prod_{j=i-n+1}^i price_j})^{\frac{1}{n}}</math>

/** The geomean.calc(stream, from, to). 

    stream the tick stream
    from   the first period of the range to calculate
    to     the last period of the range to calculate

    The function return the geometric mean value (1 number)
  */
int geomean_calc(lua_State *L)
{
    double ret = 0;

    IIndicatorTickSource *src = IndicoreAccessors::getIIndicatorTickSource(L, 1);
    if (src == 0)
        return luaL_error(L, "the first parameter must be source.");

    if (lua_isnumber(L, 2) == 0)
        return luaL_error(L, "the second parameter must be number");
    int from = (int)lua_tonumber(L, 2);

    if (lua_isnumber(L, 3) == 0)
        return luaL_error(L, "the third parameter must be number");
    int to = (int)lua_tonumber(L, 3);

    if (from < src->getFirstPeriod() || from >= src->size() ||
        to < src->getFirstPeriod() || to >= src->size())
        return luaL_error(L, "the period index is out of range");

    if (to <= from)
        return luaL_error(L, "the third parameter must be greater than the second");

    double n = (double)(to - from + 1);
    double prod = 1;

    for (int i = from; i <= to; i++)
        prod *= src->getTick(i);

    ret = pow(prod, 1.0 / n);

    lua_pushnumber(L, ret);
    return 1;
}

That's all the extension is finished. Just compile it.

Deploying and Testing

  • Put geomean.dll compiled into the same folder of the application (for example Marketscope or Indicator Debugger) where lua5.1.dll is located (usually, it is the folder where the application is installed);
  • Install the indicator;
  • Run indicator.

Known Problems

C++ Exceptions

The used version of LuaJIT has no support for C++ exceptions for Microsoft Visual C++/Win32 code. So, you must neither use try/catch right in the function which is called from Lua, nor call Lua back from inside of try/catch block.

See also about that problem: LuaJIT documentation, Discussion thread

This Article in Other Languages

Language: English  • español • français • русский • 中文 • 中文(繁體)‎