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
- Remove the strategy declaration — HaasScript scripts run as-is
-
Use
HaasNumberCollection— callClosePrices(),HighPrices(),LowPrices()instead of using built-in price variables - Split OHLC parameters — indicators that need multiple price arrays take them as separate arguments
-
Check position with enums —
GetPositionDirection()returnsPositionLong,PositionShort, orNoPosition -
Convert ATR stops to percentages —
StopLoss()takes a percentage, not a price level -
Call risk management unconditionally —
StopLoss(),TakeProfit(),TrailingStopLoss()apply to whichever position is open -
Consider the signal system — for multi-indicator strategies,
GetConsensusSignal()andGetBuySellLevelSignal()provide a cleaner structure than manual conditions