📰 Latest: HaasOnline Academy Is Back — Structured Education for Smarter Trade Bots
Account
Debugging and Troubleshooting

Testing for nil/empty

Testing for nil/empty

In HaasScript, missing data is represented by nil. Indicator collections may be empty during the first few ticks of a backtest, Load() returns nil when no saved value exists, and position data is not available when no position is open. Scripts that do not account for these cases will throw runtime errors.

This page covers the patterns for detecting and handling nil and empty values.

Checking for nil

The basic nil check uses the == and ~= operators (!= works as well):

local direction = GetPositionDirection()

if direction == NoPosition then
    Log('No open position')
end

if direction ~= NoPosition then
    Log('Position direction: ' .. tostring(direction))
end

A common shorthand exploits the fact that nil is false in Lua:

local savedValue = Load('someKey')

if savedValue then
    Log('Value exists: ' .. tostring(savedValue))
else
    Log('No saved value')
end

Both forms are equivalent. The explicit ~= nil version is more readable for beginners, while the shorthand is concise and widely used.

IfNull()

IfNull() replaces a nil value with a default. It takes two parameters and returns the first if it is not nil, otherwise the second:

local savedThreshold = Load('threshold')
local threshold = IfNull(savedThreshold, 14)

-- If savedThreshold is nil (nothing was saved), threshold becomes 14
-- If savedThreshold is 20 (a previous value was saved), threshold becomes 20

This is cleaner than writing an explicit conditional:

-- Without IfNull
local savedThreshold = Load('threshold')
local threshold
if savedThreshold == nil then
    threshold = 14
else
    threshold = savedThreshold
end

-- With IfNull
local threshold = IfNull(Load('threshold'), 14)

Common use cases:

-- Default input values from saved state
local rsiLength = IfNull(Load('rsiLength'), 14)
local stopLossPct = IfNull(Load('stopLossPct'), 2.0)

-- Default trading state
local tradeCount = IfNull(Load('tradeCount'), 0)
local lastSignal = IfNull(Load('lastSignal'), SignalNone)

Load() and nil

Load() returns nil when no value exists for the given key. This is the most common source of nil values in HaasScript:

local lastTradeTime = Load('lastTradeTime')

-- Always handle the nil case
if lastTradeTime == nil then
    Log('First run - no previous trade time')
    -- Proceed with default behavior
else
    local elapsed = Time() - lastTradeTime
    Log('Time since last trade: ' .. elapsed .. 's')
end

Combining Load() with IfNull() is the standard pattern when you need a fallback:

-- If nothing was saved, start with 0
local totalProfit = IfNull(Load('totalProfit'), 0)
totalProfit = totalProfit + currentTradeProfit
Save('totalProfit', totalProfit)

Checking Position State

GetPositionDirection() returns an enum indicating the current position state. Use it alongside PositionLong, PositionShort, and NoPosition to guard position-dependent logic:

local direction = GetPositionDirection()

if direction ~= NoPosition then
    local amount = GetPositionAmount()
    local entryPrice = GetPositionEnterPrice()
    local profit = GetPositionProfit()
    Log('Amount: ' .. tostring(amount) .. ' Entry: ' .. tostring(entryPrice) .. ' PnL: ' .. tostring(profit))
end

Attempting to call GetPositionAmount() or similar commands when no position is open will return nil or 0. Always check the direction first.

Empty Collections

HaasNumberCollections are not nil when empty. They are valid objects with zero elements. Accessing an index that does not exist returns nil:

local closePrices = ClosePrices()

-- The collection itself is not nil
Log(tostring(closePrices))  -- Always prints something

-- But individual indexes may be nil if the collection is empty
local latestClose = closePrices[1]
if latestClose == nil then
    Log('No price data available yet')
end

For most indicators, this is handled automatically because they require a minimum number of data points. For example, RSI with a period of 14 will not return a value until at least 14 candles have closed. Before that, the indicator collection is empty and accessing [1] returns nil.

Guarding Indicator Access

Wrap indicator-dependent logic in a nil check:

local rsi = RSI(ClosePrices(), 14)
local currentRSI = rsi[1]

if currentRSI ~= nil then
    if currentRSI < 30 then
        DoLong()
    end
else
    Log('Waiting for RSI to initialize')
end

Counting Collection Elements

Use Count() to check how many elements are in a collection before accessing it:

local rsi = RSI(ClosePrices(), 14)

if Count(rsi) > 0 then
    Log('RSI: ' .. rsi[1])
else
    Log('RSI not yet calculated')
end

This is useful when you need to verify that an indicator has enough history for your logic:

local ema = EMA(ClosePrices(), 50)

if Count(ema) >= 50 then
    -- Enough data for reliable EMA values
    local shortEma = EMA(ClosePrices(), 9)
    local longEma = EMA(ClosePrices(), 50)
    if shortEma > longEma then
        DoLong()
    end
end

Zero vs nil

Distinguish between "no data" (nil) and "data is zero":

local direction = GetPositionDirection()

if direction == NoPosition then
    -- No position at all
    Log('No position open')
else
    local amount = GetPositionAmount()
    if amount == nil or amount == 0 then
        -- Position direction is set but size is zero (partially filled, etc.)
        LogWarning('Position exists but amount is zero')
    else
        -- Normal position
        Log('Position size: ' .. amount)
    end
end

Validation Patterns for Trading Logic

Validate Before Trading

Check all required conditions before executing a trade:

local rsi = RSI(ClosePrices(), 14)
local currentRSI = rsi[1]

if currentRSI ~= nil and GetPositionDirection() ~= PositionLong then
    if currentRSI < 30 then
        DoLong()
    end
end

Validate Input Parameters

Ensure inputs have valid values before using them:

local rsiPeriod = Input('RSI Period', 14)

if rsiPeriod == nil or rsiPeriod < 2 then
    LogError('Invalid RSI period: ' .. tostring(rsiPeriod))
    return  -- Exit script execution for this tick
end

local rsi = RSI(ClosePrices(), rsiPeriod)

Validate Trade Amount

Before placing orders, verify the amount meets exchange minimums:

local market = PriceMarket()
local currentPrice = ClosePrices()[1]

if currentPrice == nil then
    LogWarning('Cannot place order - no price data')
    return
end

local amount = TradeAmount()
if IsTradeAmountEnough(market, currentPrice, amount, true) then
    PlaceBuyOrder(currentPrice, amount)
else
    LogError('Trade amount insufficient for exchange minimums')
end

Common Mistakes

Accessing position data without checking state first:

-- Crashes or returns garbage when no position is open
Log(GetPositionAmount())

-- Correct: check direction first
if GetPositionDirection() ~= NoPosition then
    Log(GetPositionAmount())
end

Not handling Load() returning nil:

-- Crashes if 'count' was never saved
local count = Load('count')
count = count + 1  -- nil + 1 = error

-- Correct
local count = IfNull(Load('count'), 0) + 1

Confusing nil with false:

local flag = Load('flag')

-- Wrong: false and nil both pass this check
if flag then
    Log('Flag is set')
end

-- Correct if you need to distinguish false from never-saved
if flag ~= nil then
    Log('Flag exists (may be false): ' .. tostring(flag))
end

Assuming collections are nil when empty:

local rsi = RSI(ClosePrices(), 14)

-- Wrong: the collection is never nil, only its elements can be
if rsi == nil then
    Log('No RSI')
end

-- Correct: check for nil elements or count
if rsi[1] == nil or Count(rsi) == 0 then
    Log('RSI not yet available')
end

Using parentheses on enums:

-- Wrong: enums are values, not functions
if GetPositionDirection() == PositionLong() then

-- Correct: no parentheses
if GetPositionDirection() == PositionLong then