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

Python Part 3 - Common Patterns

Python to HaasScript: Common Patterns

Python trading strategies use certain patterns repeatedly. This page shows how those patterns map conceptually to HaasScript — not line-by-line translations, but the HaasScript equivalent of what you are actually trying to accomplish.

Configuration and Constants

Python — variables at module level, or a config dict/class:

RSI_LENGTH = 14
RSI_OVERSOLD = 30
TAKE_PROFIT_MULT = 2.0

config = {
    'symbol': 'BTC/USDT',
    'timeframe': '1h',
    'dry_run': True,
}

HaasScriptInput() for user-configurable values, plain variables for internal constants:

-- User configurable (appears in bot UI)
local rsiLength = Input('RSI Length', 14)
local dryRun = Input('Dry Run', true)

-- Internal constant (not exposed in UI)
local takeProfitMult = 2.0

Use Input() for anything a user might want to change without editing code. Use plain local variables for values that are part of the strategy's logic and should not be modified by users.

Multiple Indicator Confirmation

Python — chain conditions in an if statement:

if (df['rsi'].iloc[-1] < 30
    and df['close'].iloc[-1] > df['ema_50'].iloc[-1]
    and df['macd'].iloc[-1] > df['macd_signal'].iloc[-1]):
    exchange.create_market_buy_order(symbol, amount)

HaasScript — same pattern works, but the signal system is often cleaner:

-- Direct conditions (works fine)
local rsi = RSI(ClosePrices(), 14)
local ema = EMA(ClosePrices(), 50)
local macd = MACD(ClosePrices(), 12, 26, 9)

if rsi < 30 and closePrices > ema and macd[1] > macd[3] then
    DoLong()
end
-- Signal system (better for 3+ indicators, reusable, composable)
local rsiSignal = GetBuySellLevelSignal(rsi, 30, 70)
local macdSignal = GetCrossOverUnderSignal(macd[1], macd[3])

local consensus = GetConsensusSignal(rsiSignal, macdSignal)
DoSignal(consensus)

Use direct conditions for two indicators. Use the signal system when you have three or more, or when you want to mix long/short/exit signals from different sources.

ATR-Based Dynamic Stop Loss

Python — calculate a price level and place a stop order:

atr = ta.volatility.average_true_range(df['high'], df['low'], df['close'], 14)
current_atr = atr.iloc[-1]
stop_price = entry_price - (current_atr * 2)

exchange.create_order(
    symbol, type='stop_loss', side='sell',
    amount=amount, price=stop_price
)

HaasScript — convert ATR from price to percentage, then use StopLoss():

local atr = ATR(HighPrices(), LowPrices(), ClosePrices(), 14)

-- ATR is a price value. StopLoss() takes a percentage.
-- Convert: (atr / currentPrice) * 100 = percentage
local atrPercentage = (atr / ClosePrices()) * 100
StopLoss(atrPercentage * 2)

Call this once at the top of the script, unconditionally. The bot applies it to whichever position is currently open and updates it on every tick as ATR changes.

Trailing Stop

Python — manually track the highest price and adjust the stop level:

if position is not None:
    if current_price > highest_price:
        highest_price = current_price
    trailing_stop = highest_price * (1 - trailing_pct)

    if current_price <= trailing_stop:
        exchange.create_market_sell_order(symbol, amount)
        position = None

HaasScript — single command:

TrailingStopLoss(1.5)  -- 1.5% trailing

For more control, HaasScript offers variations:

TrailingStopLoss(1.5)               -- Simple trailing
GrowingTrailingStopLoss(1.5, 0.5)   -- Widens as profit increases
ShrinkingTrailingStopLoss(1.5, 0.5) -- Tightens as profit increases
TrailingArmStopLoss(1.5, 2.0)       -- Activates only after 2% profit

Cooldown Between Trades

Python — track the last trade time and compare:

last_trade_time = 0
cooldown_seconds = 3600

if time.time() - last_trade_time > cooldown_seconds:
    # Allow trade
    exchange.create_market_buy_order(symbol, amount)
    last_trade_time = time.time()

HaasScriptTradeOncePerBar() or StopLossCooldown():

-- Only trade once per candle bar
TradeOncePerBar()

-- Wait N minutes after a safety-triggered exit
StopLossCooldown(60)

TradeOncePerBar() prevents multiple signals on the same candle. StopLossCooldown() specifically prevents re-entry after a stop loss or take profit triggered.

Multi-Timeframe Confirmation

Python — resample the dataframe:

df_1h = fetch_ohlcv(symbol, '1h')
df_4h = fetch_ohlcv(symbol, '4h')
df_4h['ema'] = ta.trend.ema_indicator(df_4h['close'], 20)

trend_bullish = df_1h['close'].iloc[-1] > df_4h['ema'].iloc[-1]

HaasScript — pass the interval parameter (in minutes) to price functions:

local hourlyClose = ClosePrices(60)
local fourHourClose = ClosePrices(240)
local fourHourEMA = EMA(fourHourClose, 20)

local trendBullish = hourlyClose > fourHourEMA

Common intervals in minutes: 1, 5, 15, 30, 60, 120, 240, 1440, 10080.

Position State Checks

Python — manual tracking:

in_position = False
position_side = None

if position is not None:
    in_position = True
    position_side = position['side']

if in_position and position_side == 'long':
    # do something

HaasScriptGetPositionDirection():

local position = GetPositionDirection()

if position == NoPosition then
    -- Not in a trade
elseif position == PositionLong then
    -- Currently long
elseif position == PositionShort then
    -- Currently short
end

Additional position queries:

GetPositionEnterPrice()   -- Your average entry price
GetPositionProfit()       -- Realized + unrealized profit
GetPositionROI()          -- ROI as a percentage
GetPositionAmount()       -- Position size

Conditional Feature Toggles

Python — boolean flags:

USE_TRAILING_STOP = True
USE_TAKE_PROFIT = False

if USE_TRAILING_STOP:
    set_trailing_stop(...)

HaasScriptInput() booleans:

local useTrailingStop = Input('Use Trailing Stop', true)
local useTakeProfit = Input('Use Take Profit', false)

if useTrailingStop then
    TrailingStopLoss(1.5)
end

if useTakeProfit then
    TakeProfit(5.0)
end

This is cleaner than editing constants in the script. Users can toggle features from the bot interface.

Logging and Debugging

Pythonprint() or the logging module:

import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

logger.info(f'RSI: {current_rsi:.2f}')
logger.warning('Position size too small')

HaasScriptLog(), LogWarning(), LogError():

Log('RSI: ' .. tostring(rsi))
LogWarning('Position size too small')
LogError('Failed to calculate indicator')

For debug-only logging, gate behind an input:

local debugMode = Input('Debug Mode', false)

if debugMode then
    Log('RSI: ' .. tostring(rsi))
    Log('EMA: ' .. tostring(ema))
    Log('ATR%: ' .. tostring(atrPercentage))
end

Recording Metrics

Python — write to a file or database:

with open('trades.csv', 'a') as f:
    f.write(f'{timestamp},{side},{profit}\n')

HaasScriptCustomReport() for backtest metrics, Save()/Load() for persistent counters:

-- Backtest metrics (appear in backtest results)
CustomReport('Total Trades', tradeCount)
CustomReport('Win Rate', winRate .. '%')

-- Persistent counter between ticks
local totalSignals = Load('totalSignals', 0)
totalSignals = totalSignals + 1
Save('totalSignals', totalSignals)

CustomReport() values appear in the backtest report alongside built-in fields. Save()/Load() persists data across ticks but does not survive bot restarts — use it for runtime counters, not permanent records.

Crossunder / Crossover Detection

Python — compare current and previous values:

prev_macd = df['macd'].iloc[-2]
curr_macd = df['macd'].iloc[-1]
prev_signal = df['macd_signal'].iloc[-2]
curr_signal = df['macd_signal'].iloc[-1]

bullish_cross = prev_macd < prev_signal and curr_macd > curr_signal
bearish_cross = prev_macd > prev_signal and curr_macd < curr_signal

HaasScriptCrossOver() and CrossUnder():

local macd = MACD(ClosePrices(), 12, 26, 9)

if CrossOver(macd[1], macd[3]) then
    -- MACD line crossed above signal line
    DoLong()
end

if CrossUnder(macd[1], macd[3]) then
    -- MACD line crossed below signal line
    DoShort()
end

For time since the last crossover (useful for filters):

local barsSinceCross = CrossOverSince(macd[1], macd[3])
if barsSinceCross > 3 then
    -- Crossover happened more than 3 bars ago
end

Error Handling

Python — try/except:

try:
    order = exchange.create_market_buy_order(symbol, amount)
except ccxt.InsufficientFunds:
    logging.error('Insufficient funds')
except ccxt.NetworkError:
    logging.error('Network error, retrying')

HaasScriptIfNull() for nil checks, LogWalletError() for trade amount problems:

local rsi = RSI(ClosePrices(), 14)

-- Handle missing data
local threshold = IfNull(rsi, 50)

-- Flag wallet/balance issues (stops trade execution until resolved)
local tradeAmount = TradeAmount()
local minAmount = MinimumTradeAmount()
if tradeAmount < minAmount then
    LogWalletError('Trade amount too small: ' .. tostring(tradeAmount))
end

HaasScript does not have try/catch. Exchange errors, network issues, and order failures are handled by the TradeServer platform. Your script handles data validation and business logic errors.

Highest / Lowest in a Window

Python — rolling window on a dataframe:

df['highest_20'] = df['high'].rolling(20).max()
df['lowest_20'] = df['low'].rolling(20).min()

current_high = df['highest_20'].iloc[-1]
current_low = df['lowest_20'].iloc[-1]

HaasScriptGetHigh() and GetLow():

local highest20 = GetHigh(HighPrices(), 20)   -- Highest high in 20 periods
local lowest20 = GetLow(LowPrices(), 20)      -- Lowest low in 20 periods

if ClosePrices() >= highest20 then
    Log('20-period high broken')
end

For full rolling series (not just the single highest/lowest value), use GetHighs() and GetLows() which return HaasNumberCollection.