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
positiondictionary —GetPositionDirection()returns an enum (PositionLong,PositionShort,NoPosition) -
No order placement —
DoLong()/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/elseifhandles 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:
-
ATR returns a price,
StopLoss()takes a percentage. Convert with(atr / currentPrice) * 100. This is the most common mistake Python traders make when converting. -
Risk management is unconditional. Do not wrap
StopLoss()in anif positionblock. 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 |