ScriptingLanguageExecution model

Execution Model

The execution model of Lipi Script runtime is closely connected to Lipi Script‘s time series and type system. A comprehensive understanding of these three components is essential to harness the full potential of Lipi Script.

The execution model dictates how scripts are run on charts and, in turn, how the code within these scripts functions. Lipi Script‘s runtime enables your code to operate on charts after compiling, executing it whenever an event triggers the script.

When a Lipi Script script is loaded onto a chart, it runs once for each historical bar, leveraging the OHLCV (open, high, low, close, volume) values available for each bar. Once the script reaches the most recent bar in the dataset, it will continue executing whenever an update occurs, such as a price or volume change, if trading is active. While Lipi Script indicators execute on each update by default, Lipi Script strategies run at the close of each bar by default but can be configured to update with each change, similar to indicators.

Each symbol/timeframe pair has a dataset containing a limited number of bars. As you scroll a chart to view earlier bars in the dataset, those bars are loaded on the chart until either there are no more bars for the selected symbol/timeframe or the maximum number of bars allowed by your account type has been loaded. The first bar in the dataset, viewable by scrolling left, has an index value of 0.

Initially, all bars in a dataset are historical, except for the most recent one if a trading session is active. This active, rightmost bar is known as the realtime bar, which updates with each price or volume change. Once the realtime bar closes, it becomes an elapsed realtime bar, and a new realtime bar opens.

Calculation based on historical bars

Let’s take a simple script and follow its execution on historical bars:

indicator("My Script", (overlay = true));
src = close;
a = talib.sma(src, 5);
b = talib.sma(src, 50);
c = talib.cross(a, b);
plot(a, (color = color.blue));
plot(b, (color = color.black));
plotshape(c, (color = color.red));

On historical bars, a script executes as if it’s at the bar’s close, when the OHLCV values (open, high, low, close, volume) are fully known for that bar. Before a script runs on a bar, the built-in variables like open, high, low, close, volume, and time are initialized with values specific to that bar. A script executes once for each historical bar.

In our example, the script begins execution on the very first bar in the dataset, with an index value of 0. Each statement runs using values from the current bar. Therefore, on the first bar in the dataset, the following statement:

src = close;

The script initializes the variable src with the close value of the first bar, and each subsequent line is executed in sequence. Since the script executes only once per historical bar, it always calculates using the same close value for that specific bar.

As each line in the script executes, it performs calculations that produce the indicator’s output values, which can then be plotted on the chart. In our example, the plot and plotshape functions at the end of the script are used to display some of these values. For a strategy, the result of these calculations can either be plotted or used to determine orders.

After executing and plotting on the first bar, the script moves to the dataset’s second bar, with an index of 1. This process repeats until all historical bars in the dataset are processed and the script reaches the chart’s rightmost bar.

image

Calculation Based on Realtime Bars

The behavior of a Lipi Script on the realtime bar is significantly different from its behavior on historical bars. The realtime bar is the rightmost bar on the chart when trading is active for the chart’s symbol. Additionally, strategies can operate in two distinct ways on the realtime bar. By default, they only execute when the realtime bar closes. However, setting the calc_on_every_tick parameter in the strategy declaration to true modifies the strategy’s behavior to execute on every update, just like indicators. The behavior described here applies to strategies only when calc_on_every_tick=true.

The key difference between script execution on historical and realtime bars is that, while scripts execute only once on each historical bar, they execute every time an update occurs during a realtime bar. This means that built-in variables like high, low, and close—which remain constant on a historical bar—can change with each script iteration on the realtime bar. As these built-in variables change during updates, the calculations in the script also change, allowing it to reflect realtime price action. As a result, the same script may produce different outputs every time it executes during the realtime bar.

Note: On the realtime bar, the close variable always represents the current price. Likewise, the high and low variables represent the highest and lowest prices reached since the start of the realtime bar. Lipi Script’s built-in variables will show the final values of the realtime bar only on its last update.

Let’s walk through our example script on the realtime bar.

When the script reaches the realtime bar, it executes once initially, using the current values of built-in variables to produce results and, if necessary, plots them. Before the script executes again on the next update, its user-defined variables reset to a known state from the last commit at the close of the previous bar. If variables are initialized each bar, they will reinitialize here, losing their last calculated state. Similarly, plotted labels and lines are reset. This resetting of user-defined variables and drawings before each iteration on the realtime bar is called rollback, ensuring calculations in the realtime bar always start from a clean state.

With constant recalculations as price or volume changes, the script may produce different outputs within the same realtime bar. For instance, if a variable like c in our example becomes true because of a price cross, a red marker plotted in the script’s last line will appear on the chart. If the price updates again and no longer meets the condition for c (due to a price movement), then the previously plotted marker will disappear.

When the realtime bar closes, the script executes one final time. Variables undergo rollback before this last execution. However, since this is the final iteration on the realtime bar, variables are committed to their final values for that bar after calculations are completed.

Realtime Bar Process Summary:

  • A script executes at the open of the realtime bar and once per update.
  • Variables undergo rollback before each update.
  • Variables are committed once during the closing update of the realtime bar.

Events Triggering the Execution of a Script

A script executes across the full set of bars on the chart whenever one of the following events occurs:

  • A new symbol or timeframe is loaded on the chart.
  • A script is saved or added to the chart, whether from the Lipi Script Editor or through the chart’s Indicators & Strategies dialog box.
  • A value is modified in the script’s Settings/Inputs dialog box.
  • A value is adjusted in a strategy’s Settings/Properties dialog box.
  • A browser refresh event takes place.

When trading is active, a script will execute on the realtime bar under the following conditions:

  • One of the above events triggers the script to execute on the open of the realtime bar, or
  • The realtime bar updates due to a detected change in price or volume.

It’s important to note that when a chart remains untouched while the market is active, a series of realtime bars that have opened and then closed may trail the current realtime bar. These elapsed realtime bars are confirmed, with all variables committed, yet the script has not executed on them in their historical state since they did not exist as historical bars during the script’s last execution on the chart’s dataset.

When an event triggers script execution on the chart and causes it to run on bars that have now become historical bars, the calculations may occasionally differ from those performed on the last update when these bars were realtime bars. This discrepancy can arise from slight differences between the OHLCV values saved at the close of realtime bars and the values fetched from data feeds when these bars become historical bars. This behavior is one possible cause of repainting.


More Information

The built-in barstate. variables offer insights into the type of bar or the event during script execution. The documentation on these variables includes a script that can help visualize the distinction between elapsed realtime and historical bars.

For further details on strategy calculations, which differ from indicator calculations, refer to the Strategies page.

Historical Values of Functions

Each function call in Lipi Script leaves a trace of historical values that can be accessed on subsequent bars using the [] operator. The historical series of functions is determined by consecutive calls that record the output on every bar. If a function is not called on every bar, it can create an inconsistent history, which may affect calculations and results, particularly when the continuity of these historical series is essential for the expected operation. The compiler provides warnings in such instances to alert users that the values from a function—whether built-in or user-defined—might be misleading.

To illustrate this concept, let’s create a script that calculates the index of the current bar and outputs that value on every second bar. In the following script, we define a calcBarIndex() function that increments the previous value of its internal index variable on every bar. The script calls this function on each bar where the condition returns true (every other bar) to update the customIndex value. It then plots this value alongside the built-in bar_index to validate the output:

image

indicator("My script")
 
//@function Calculates the index of the current bar by adding 1 to its own value from the previous bar.
// The first bar will have an index of 0.
calcBarIndex() {
int index = na
index := nz(index[1], alt = -1) + 1
return index
}
 
 
//@variable Returns `true` on every other bar.
condition = bar_index % 2 == 0
 
int customIndex = na
 
// Call `calcBarIndex()` when the `condition` is `true`. This prompts the compiler to raise a warning.
if condition  {
    customIndex := calcBarIndex()
}
plot(bar_index,   "Bar index",    color = color.green)
plot(customIndex, "Custom index", color = color.red, style = plotStyle.cross)

Note that:

The nz() function is designed to replace na values with a specified replacement value, which defaults to 0. On the first bar of the script, when the index series lacks history, the na value is substituted with -1 before adding 1, resulting in an initial value of 0.

Upon examining the chart, we notice that the two plots vary significantly. This discrepancy arises because the script calls calcBarIndex() within the scope of an if structure on every other bar, leading to a historical output that is inconsistent with the bar_index series. By calling the function once every two bars, it references the previous index value from two bars ago, specifically the last bar on which the function executed. This behavior causes the customIndex value to be half that of the built-in bar_index.

To align the output of calcBarIndex() with the bar_index, we can move the function call to the global scope of the script. This adjustment ensures that the function executes on every bar, allowing for the entire history to be recorded and referenced, rather than only the results from every other bar. In the code below, we define a globalScopeBarIndex variable in the global scope and assign it to the return value from calcBarIndex() instead of calling the function locally. The script then sets the customIndex to the value of globalScopeBarIndex when the specified condition occurs:

indicator("New Concept script")
 
//@function Calculates the index of the current bar by adding 1 to its own value from the previous bar.
// The first bar will have an index of 0.
calcBarIndex() {
    int index = na
    index := nz(index[1], alt = -1) + 1
    return index
}
 
//@variable Returns `true` on every second bar.
condition = bar_index % 2 == 0
 
globalScopeBarIndex = calcBarIndex()
int customIndex = na
 
// Assign `customIndex` to `globalScopeBarIndex` when the `condition` is `true`. This won't produce a warning.
if condition {
    customIndex := globalScopeBarIndex
}
 
plot(bar_index,   "Bar index",    color = color.green)
plot(customIndex, "Custom index", color = color.red, style = plotStyle.cross)

Impact of Historical References on Built-in Functions

This behavior can significantly affect built-in functions that rely on historical data internally. For instance, the talib.sma() function uses its past values “under the hood.” If a script calls this function conditionally, rather than on every bar, the values involved in the calculation may vary greatly. To ensure consistent calculations, we can assign talib.sma() to a variable in the global scope and reference that variable’s history as needed.

The following example calculates three SMA series: controlSMA, localSMA, and globalSMA. The script computes controlSMA in the global scope and localSMA within the local scope of an if structure. Inside this if structure, it also updates the value of globalSMA using the controlSMA value. As demonstrated, the values from the globalSMA and controlSMA series align, while the localSMA series diverges from the other two due to its reliance on incomplete history, which impacts its calculations:

image

indicator("My script")
 
//@variable Returns `true` on every second bar.
condition = bar_index % 2 == 0
controlSMA = talib.sma(close, 20)
float globalSMA = na
float localSMA  = na
 
// Update `globalSMA` and `localSMA` when `condition` is `true`.
if condition {
      // No warning.
    globalSMA := controlSMA   
      // Raises warning. This function depends on its history to work as intended.     
    localSMA  := talib.sma(close, 20) 
}
plot(controlSMA, "Control SMA", color = color.green)
plot(globalSMA,  "Global SMA",  color = color.blue, style = plotStyle.cross)
plot(localSMA,   "Local SMA",   color = color.red,  style = plotStyle.cross)

Understanding Function Behavior

Why does this behavior occur? This behavior is necessary because requiring the execution of functions on every bar could lead to unexpected results in functions that produce side effects—those that do more than just return a value. For instance, the label.new() function creates a label on the chart; if it were called on every bar, even when contained within an if structure, it would result in labels appearing where they logically shouldn’t.

Exceptions

Not all built-in functions utilize their previous values in their calculations, which means that not all require execution on every bar. For example, math.max() compares all its arguments to return the highest value. Functions that do not interact with their history in any manner do not need special handling.

If the use of a function within a conditional block does not trigger a compiler warning, it is safe to use it without affecting calculations. If a warning is present, consider moving the function call to the global scope to ensure consistent execution. When keeping a function call within a conditional block despite receiving a warning, make sure the output is correct to avoid unexpected results.