📰 Latest: HaasOnline Academy Is Back — Structured Education for Smarter Trade Bots
Account
Converting Other Languages

PineScript Part 2 - Strategy Conversion

PineScript Strategy Conversion

This page walks through converting a PineScript strategy to HaasScript step by step. The starting strategy uses RSI for entry signals, EMA for trend filtering, and ATR-based risk management — a common pattern that demonstrates the key differences between the two languages.

The PineScript Strategy

//@version=5
strategy("RSI + EMA Trend", overlay=true, default_qty_type=strategy.percent_of_equity, default_qty_value=100)

// Inputs
rsiLength = input.int(14, "RSI Length")
rsiOversold = input.int(30, "Oversold")
rsiOverbought = input.int(70, "Overbought")
emaLength = input.int(50, "EMA Trend Filter")
atrLength = input.int(14, "ATR Length")
atrMultiplier = input.float(2.0, "ATR Stop Loss Multiplier")

// Indicators
rsi = ta.rsi(close, rsiLength)
ema = ta.ema(close, emaLength)
atr = ta.atr(atrLength)

// Logic
longCondition = rsi < rsiOversold and close > ema
shortCondition = rsi > rsiOverbought and close < ema

// Entry
if longCondition and strategy.position_size == 0
    strategy.entry("Long", strategy.long)

if shortCondition and strategy.position_size == 0
    strategy.entry("Short", strategy.short)

// Risk management
slDistance = atr * atrMultiplier
if strategy.position_size > 0
    strategy.exit("SL", "Long", stop=close - slDistance)

if strategy.position_size < 0
    strategy.exit("SL", "Short", stop=close + slDistance)

// Plotting
plot(ema, "EMA", color=color.blue)
bgcolor(rsi < rsiOversold ? color.new(color.green, 90) : na)
bgcolor(rsi > rsiOverbought ? color.new(color.red, 90) : na)

Step 1: Script Declaration

PineScript requires strategy() at the top. HaasScript does not — remove it entirely.

// Remove this line
strategy("RSI + EMA Trend", overlay=true, default_qty_type=strategy.percent_of_equity, default_qty_value=100)

Position sizing is handled by the bot configuration, not the script. The overlay=true concept does not exist in HaasScript — every script can plot to any chart area using explicit commands.

Step 2: Inputs

Convert PineScript input.*() calls to HaasScript Input(). The type is auto-detected from the default value:

local rsiLength = Input('RSI Length', 14)
local rsiOversold = Input('Oversold', 30)
local rsiOverbought = Input('Overbought', 70)
local emaLength = Input('EMA Trend Filter', 50)
local atrLength = Input('ATR Length', 14)
local atrMultiplier = Input('ATR Stop Loss Multiplier', 2.0)

No input.int() vs input.float() distinction needed — Input() handles both.

Step 3: Indicators

PineScript uses ta.* namespace with a single close variable. HaasScript uses top-level functions that accept HaasNumberCollection. The key change is using ClosePrices(), HighPrices(), LowPrices() instead of built-in variables:

local closePrices = ClosePrices()
local highPrices = HighPrices()
local lowPrices = LowPrices()

local rsi = RSI(closePrices, rsiLength)
local ema = EMA(closePrices, emaLength)
local atr = ATR(highPrices, lowPrices, closePrices, atrLength)

Note that ATR() requires three separate arrays (high, low, close) instead of a single source. This is common for HaasScript indicators that need OHLC data — see the Common Parameter Patterns section.

Step 4: Entry Conditions

PineScript uses strategy.position_size == 0 to check for no position. HaasScript uses GetPositionDirection():

local position = GetPositionDirection()

The condition logic converts directly, but the position check uses an enum instead of a numeric comparison:

local longCondition = rsi < rsiOversold and closePrices > ema
local shortCondition = rsi > rsiOverbought and closePrices < ema

if longCondition and position == NoPosition then
    DoLong()
end

if shortCondition and position == NoPosition then
    DoShort()
end

Using NoPosition instead of checking for zero avoids ambiguity — there is no numeric position size to compare against.

Step 5: Risk Management

PineScript passes a price level to strategy.exit(). HaasScript's StopLoss() accepts a percentage, not a price. Convert the ATR distance to a percentage first:

local atrPercentage = (atr / closePrices) * 100
StopLoss(atrPercentage * atrMultiplier)

This is a common pattern in HaasScript. ATR returns a price value, but StopLoss() expects a percentage relative to your entry price. The conversion is: (atrPrice / currentPrice) * 100 = atrAsPercentage.

Risk management commands like StopLoss(), TakeProfit(), and TrailingStopLoss() are called unconditionally (outside of if blocks). The bot applies them automatically to whichever position is currently open:

StopLoss(atrPercentage * atrMultiplier)
TakeProfit(atrPercentage * atrMultiplier * 2)

You do not need separate stop logic for long and short positions — the platform handles direction internally.

Step 6: Plotting

Convert PineScript's plot commands:

Plot(ema, 'EMA', 'blue')

PineScript's bgcolor() with transparency maps to MarkCandle() in HaasScript:

-- PineScript: semi-transparent background color
bgcolor(rsi < rsiOversold ? color.new(color.green, 90) : na)

-- HaasScript: mark the candle with a color
if rsi < rsiOversold then
    MarkCandle(true, 'rgba(0, 255, 0, 0.15)')
end

The Complete HaasScript Strategy

-- Inputs
local rsiLength = Input('RSI Length', 14)
local rsiOversold = Input('Oversold', 30)
local rsiOverbought = Input('Overbought', 70)
local emaLength = Input('EMA Trend Filter', 50)
local atrLength = Input('ATR Length', 14)
local atrMultiplier = Input('ATR Stop Loss Multiplier', 2.0)

-- Price data
local closePrices = ClosePrices()
local highPrices = HighPrices()
local lowPrices = LowPrices()

-- Indicators
local rsi = RSI(closePrices, rsiLength)
local ema = EMA(closePrices, emaLength)
local atr = ATR(highPrices, lowPrices, closePrices, atrLength)

-- Risk management (unconditional)
local atrPercentage = (atr / closePrices) * 100
StopLoss(atrPercentage * atrMultiplier)
TakeProfit(atrPercentage * atrMultiplier * 2)

-- Entry logic
local position = GetPositionDirection()

local longCondition = rsi < rsiOversold and closePrices > ema
local shortCondition = rsi > rsiOverbought and closePrices < ema

if longCondition and position == NoPosition then
    DoLong()
end

if shortCondition and position == NoPosition then
    DoShort()
end

-- Plotting
Plot(ema, 'EMA', 'blue')
Plot(rsi, 'RSI', 'purple')

if rsi < rsiOversold then
    MarkCandle(true, 'rgba(0, 255, 0, 0.15)')
end

if rsi > rsiOverbought then
    MarkCandle(true, 'rgba(255, 0, 0, 0.15)')
end

Using the Signal System Instead

HaasScript has a built-in signal system that provides a structured alternative to writing manual entry conditions. The same strategy can be expressed using GetBuySellLevelSignal() for the RSI and GetCrossOverUnderSignal() for the EMA trend filter:

-- Indicators
local closePrices = ClosePrices()
local highPrices = HighPrices()
local lowPrices = LowPrices()

local rsi = RSI(closePrices, 14)
local ema = EMA(closePrices, 50)

-- RSI signal: below 30 = long, above 70 = short
local rsiSignal = GetBuySellLevelSignal(rsi, 30, 70)

-- EMA trend signal: price above EMA = long, below = short
local emaSignal = GetCrossOverUnderSignal(closePrices, ema)

-- Both signals must agree
local consensus = GetConsensusSignal(rsiSignal, emaSignal)

-- Execute
if consensus ~= SignalNone then
    DoSignal(consensus)
end

GetConsensusSignal() requires a majority agreement. With two signals, both must align — if RSI says long but EMA says short, no trade executes. This replaces manual if/and conditions with a composable, reusable pattern.

For more complex strategies with three or more signals, GetWeightedConsensusSignal() lets you assign different weights to each indicator.

Key Takeaways

  1. Remove the strategy declaration — HaasScript scripts run as-is
  2. Use HaasNumberCollection — call ClosePrices(), HighPrices(), LowPrices() instead of using built-in price variables
  3. Split OHLC parameters — indicators that need multiple price arrays take them as separate arguments
  4. Check position with enumsGetPositionDirection() returns PositionLong, PositionShort, or NoPosition
  5. Convert ATR stops to percentagesStopLoss() takes a percentage, not a price level
  6. Call risk management unconditionallyStopLoss(), TakeProfit(), TrailingStopLoss() apply to whichever position is open
  7. Consider the signal system — for multi-indicator strategies, GetConsensusSignal() and GetBuySellLevelSignal() provide a cleaner structure than manual conditions