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

Python Part 2 - Strategy Conversion

Python Strategy Conversion

This page walks through converting a Python RSI + EMA strategy to HaasScript. The Python version uses ccxt for exchange access and pandas for data processing — the two most common libraries in Python trading strategies.

The Python Strategy

import ccxt
import pandas as pd
import ta

exchange = ccxt.binance({
    'apiKey': 'your_key',
    'secret': 'your_secret'
})

symbol = 'BTC/USDT'
timeframe = '1h'
rsi_length = 14
rsi_oversold = 30
rsi_overbought = 70
ema_length = 50
atr_length = 14
atr_multiplier = 2.0

position = None
stop_price = None
tp_price = None

def fetch_and_process():
    global position, stop_price, tp_price

    # Fetch OHLCV data
    ohlcv = exchange.fetch_ohlcv(symbol, timeframe, limit=200)
    df = pd.DataFrame(ohlcv, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
    df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')

    # Calculate indicators
    df['rsi'] = ta.momentum.rsi(df['close'], window=rsi_length)
    df['ema'] = ta.trend.ema_indicator(df['close'], window=ema_length)
    df['atr'] = ta.volatility.average_true_range(
        df['high'], df['low'], df['close'], window=atr_length
    )

    current = df.iloc[-1]
    current_price = current['close']
    current_rsi = current['rsi']
    current_ema = current['ema']
    current_atr = current['atr']

    # No position — check for entry
    if position is None:
        if current_rsi < rsi_oversold and current_price > current_ema:
            amount = 0.01
            order = exchange.create_market_buy_order(symbol, amount)
            position = {
                'side': 'long',
                'entry_price': order['average'],
                'amount': amount,
            }
            stop_price = current['close'] - (current_atr * atr_multiplier)
            tp_price = current['close'] + (current_atr * atr_multiplier * 2)

        elif current_rsi > rsi_overbought and current_price < current_ema:
            amount = 0.01
            order = exchange.create_market_sell_order(symbol, amount)
            position = {
                'side': 'short',
                'entry_price': order['average'],
                'amount': amount,
            }
            stop_price = current['close'] + (current_atr * atr_multiplier)
            tp_price = current['close'] - (current_atr * atr_multiplier * 2)

    # In position — check for exit
    elif position is not None:
        if position['side'] == 'long' and current_price <= stop_price:
            exchange.create_market_sell_order(symbol, position['amount'])
            position = None

        elif position['side'] == 'short' and current_price >= stop_price:
            exchange.create_market_buy_order(symbol, position['amount'])
            position = None

        elif current_price >= tp_price and position['side'] == 'long':
            exchange.create_market_sell_order(symbol, position['amount'])
            position = None

        elif current_price <= tp_price and position['side'] == 'short':
            exchange.create_market_buy_order(symbol, position['amount'])
            position = None

Step 1: Remove Infrastructure Code

The entire ccxt exchange setup, API keys, and fetch_ohlcv() call disappear. HaasScript does not connect to exchanges — the TradeServer platform handles that. The price data your script needs is available through built-in commands:

# Remove all of this
exchange = ccxt.binance({'apiKey': '...', 'secret': '...'})
ohlcv = exchange.fetch_ohlcv(symbol, timeframe, limit=200)
df = pd.DataFrame(ohlcv, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])

Replace it with:

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

Step 2: Convert Indicators

The Python strategy uses the ta library on a dataframe. Each indicator call is a separate column assignment:

df['rsi'] = ta.momentum.rsi(df['close'], window=rsi_length)
df['ema'] = ta.trend.ema_indicator(df['close'], window=ema_length)
df['atr'] = ta.volatility.average_true_range(
    df['high'], df['low'], df['close'], window=atr_length
)

current_rsi = df.iloc[-1]['rsi']

In HaasScript, indicator functions return the latest value directly:

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

No dataframe, no .iloc[-1] access. The function call is the current value. If you need previous values, use index access — rsi[2] is one tick ago.

The indicator names are the same (RSI, EMA, ATR). The main adjustment is that ATR() takes three separate arrays instead of a dataframe:

# Python: one dataframe, multiple columns referenced
ta.volatility.average_true_range(df['high'], df['low'], df['close'], window=14)

-- HaasScript: three separate collections
ATR(highPrices, lowPrices, closePrices, atrLength)

Step 3: Convert Entry Logic

The Python version manually tracks position state with a position variable and places orders through the exchange:

if position is None:
    if current_rsi < rsi_oversold and current_price > current_ema:
        order = exchange.create_market_buy_order(symbol, amount)
        position = {'side': 'long', 'entry_price': order['average'], 'amount': amount}
        stop_price = current['close'] - (current_atr * atr_multiplier)
        tp_price = current['close'] + (current_atr * atr_multiplier * 2)

HaasScript handles position tracking and order execution. Your script generates signals:

local position = GetPositionDirection()

if position == NoPosition then
    if rsi < rsiOversold and closePrices > ema then
        DoLong()
    elseif rsi > rsiOverbought and closePrices < ema then
        DoShort()
    end
end

Key differences:

  • No position dictionaryGetPositionDirection() returns an enum (PositionLong, PositionShort, NoPosition)
  • No order placementDoLong() / DoShort() tell the bot to execute. Position sizing is configured in the bot, not in code
  • No separate long/short entry blocks needed — the if/elseif handles both directions

Step 4: Convert Risk Management

This is the biggest conceptual shift. The Python strategy manually calculates stop and take-profit prices, stores them as variables, and checks price against them on every tick:

# Set stops on entry
stop_price = current['close'] - (current_atr * atr_multiplier)
tp_price = current['close'] + (current_atr * atr_multiplier * 2)

# Check stops on every tick
if position['side'] == 'long' and current_price <= stop_price:
    exchange.create_market_sell_order(symbol, position['amount'])
    position = None

HaasScript handles this declaratively. You set the risk parameters once, and the bot monitors them automatically:

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

Two things to note:

  1. ATR returns a price, StopLoss() takes a percentage. Convert with (atr / currentPrice) * 100. This is the most common mistake Python traders make when converting.

  2. Risk management is unconditional. Do not wrap StopLoss() in an if position block. Call it once at the top of your script and the bot applies it whenever a position is open. There is no need for separate long/short stop logic — the platform handles direction internally.

Step 5: Convert Configuration

Python uses variables at the top of the file or a configuration object:

rsi_length = 14
rsi_oversold = 30
rsi_overbought = 70
ema_length = 50

HaasScript uses Input() to expose these as configurable fields in the bot UI:

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)

Users can change these values without editing the script.

Step 6: Remove the Loop

The Python strategy is wrapped in a while True or sleep() loop that runs on a timer, or triggered by a scheduler. The global keyword persists state between iterations.

# Remove the loop and scheduler
while True:
    fetch_and_process()
    time.sleep(3600)  # Run every hour

HaasScript does not loop. The platform invokes your script on each tick or interval. State between invocations uses Save() and Load(), not global variables:

local tradeCount = Load('tradeCount', 0)
tradeCount = tradeCount + 1
Save('tradeCount', tradeCount)

For this specific strategy, no persistent state is needed — the bot tracks position and risk management internally.

The Complete HaasScript Strategy

-- Configuration
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 — applies to any open position)
local atrPercentage = (atr / closePrices) * 100
StopLoss(atrPercentage * atrMultiplier)
TakeProfit(atrPercentage * atrMultiplier * 2)

-- Entry logic
local position = GetPositionDirection()

if position == NoPosition then
    if rsi < rsiOversold and closePrices > ema then
        DoLong()
    elseif rsi > rsiOverbought and closePrices < ema then
        DoShort()
    end
end

The Python strategy was roughly 80 lines including exchange setup, dataframe processing, and manual position management. The HaasScript version is 28 lines of pure trading logic.

What Changed and Why

Aspect Python HaasScript Reason
Data fetching ccxt.fetch_ohlcv() ClosePrices() Platform provides data
Indicator calc ta.rsi(df['close'], 14) RSI(closePrices, 14) Indicators are built-in commands
Entry exchange.create_market_buy_order() DoLong() Platform executes trades
Position state Manual position dict GetPositionDirection() Platform tracks positions
Stop loss Manual price check + create_market_sell_order() StopLoss(percentage) Platform monitors and executes
Take profit Manual price check + create_market_sell_order() TakeProfit(percentage) Platform monitors and executes
Configuration Python variables Input() fields Exposed in bot UI
Loop/scheduler while True + time.sleep() None needed Platform invokes on schedule
State persistence global variables Save() / Load() Platform manages lifecycle