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