Comparisons
Naxbot isn’t your only choice when it comes to automated trading custom strategies on (crypto) exchanges. This page goes over the most popular choices and elaborates on how Naxbot compares to them.
If you’re only interested in the quick takeaways:
- Unlike all other bots, Naxbot features a native backend (most performant)
- Unlike all other bots, Naxbot lets you visually plot your indicators on the chart
- Unlike all other bots, Naxbot is fully multi-threaded
- Unlike all other bots, Naxbot lets you write custom modules to hook up to any exchange you want (crypto or otherwise)
freqtrade
freqtrade is an open source self-hosted crypto trading bot, written in the Python programming language. As of 2025-04-28, it officially supports 10 exchanges for spot trading, and 5 exchanges for futures trading.
Like Naxbot, freqtrade uses the ccxt library under the hood, so it may support many more exchanges, but they haven’t
been officially verified to work (yet).
Strategies are programmed in Python, and can also be auto-optimized by freqtrade. Compared to Naxbot, the syntax is more verbose:
from freqtrade.strategy import IStrategy
from pandas import DataFrame
import talib.abstract as ta
class MyStrategy(IStrategy):
timeframe = '15m'
# set the initial stoploss to -10%
stoploss = -0.10
# exit profitable positions at any time when the profit is greater than 1%
minimal_roi = {"0": 0.01}
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# generate values for technical analysis indicators
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# generate entry signals based on indicator values
dataframe.loc[
(dataframe['rsi'] < 30),
'enter_long'] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# generate exit signals based on indicator values
dataframe.loc[
(dataframe['rsi'] > 70),
'exit_long'] = 1
return dataframefreqtrade features AI functionality to aid traders in crafting their strategies, and can be controlled via Telegram or WebUI.
Overall, freqtrade is more aimed at people with high technical expertise and ideally prior software development
experience. Although freqtrade is written in Python, it uses numpy for most of its calculations, which has a native
backend implemented, meaning that even though single-threaded performance still won’t be on par with Naxbot, it is still
not bad.
It does, however, not make use of multi-threading or parallelism when trading.
Feature Support Matrix
| Feature | Naxbot | freqtrade |
|---|---|---|
| Custom Strategies | (Lua) | (Python) |
| Automatic Strategy Optimizing | ||
| Backend | Rust (native) | Python & Numpy |
| Multi-Threading | ||
| Backtesting | ||
| Visual Indicator Plotting | ||
| Web UI | ||
| Supported Exchanges | ccxt, extensible via custom modules | ccxt |
OctoBot
Like freqtrade, OctoBot is another open source self-hosted crypto trading bot written in Python. It also makes use of
the ccxt library under the hood, and thus supports many well-known exchanges right out of the box.
Strategies are also programmed in Python, which again makes it a little more verbose than Naxbot:
import asyncio
import tulipy # Can be any TA library.
import octobot_script as obs
async def rsi_test():
async def strategy(ctx):
# Will be called at each candle.
if run_data["entries"] is None:
# Compute entries only once per backtest.
closes = await obs.Close(ctx, max_history=True)
times = await obs.Time(ctx, max_history=True, use_close_time=True)
rsi_v = tulipy.rsi(closes, period=ctx.tentacle.trading_config["period"])
delta = len(closes) - len(rsi_v)
# Populate entries with timestamps of candles where RSI is
# below the "rsi_value_buy_threshold" configuration.
run_data["entries"] = {
times[index + delta]
for index, rsi_val in enumerate(rsi_v)
if rsi_val < ctx.tentacle.trading_config["rsi_value_buy_threshold"]
}
await obs.plot_indicator(ctx, "RSI", times[delta:], rsi_v, run_data["entries"])
if obs.current_live_time(ctx) in run_data["entries"]:
# Uses pre-computed entries times to enter positions when relevant.
# Also, instantly set take profits and stop losses.
# Position exists could also be set separately.
await obs.market(ctx, "buy", amount="10%", stop_loss_offset="-15%", take_profit_offset="25%")
# Configuration that will be passed to each run.
# It will be accessible under "ctx.tentacle.trading_config".
config = {
"period": 10,
"rsi_value_buy_threshold": 28,
}
# Read and cache candle data to make subsequent backtesting runs faster.
data = await obs.get_data("BTC/USDT", "1d", start_timestamp=1505606400)
run_data = {
"entries": None,
}
# Run a backtest using the above data, strategy and configuration.
res = await obs.run(data, strategy, config)
print(res.describe())
# Generate and open report including indicators plots
await res.plot(show=True)
# Stop data to release local databases.
await data.stop()
# Call the execution of the script inside "asyncio.run" as
# OctoBot script runs using the python asyncio framework.
asyncio.run(rsi_test())Additionally, OctoBot makes it possible to plot indicators, albeit only in a non-visual manner (not on the chart).
In contrast to freqtrade and Naxbot, OctoBot does not have a built-in strategy optimizer.
Feature Support Matrix
| Feature | Naxbot | OctoBot |
|---|---|---|
| Custom Strategies | (Lua) | (Python) |
| Automatic Strategy Optimizing | ||
| Backend | Rust (native) | Python & Numpy |
| Multi-Threading | ||
| Backtesting | ||
| Visual Indicator Plotting | ||
| Web UI | ||
| Supported Exchanges | ccxt, extensible via custom modules | ccxt |
Gunbot
Gunbot is an established self-hosted crypto trading bot written in JavaScript. It has been a popular solution among the community for many years, and was actually the inspiration for Naxbot!
Unlike OctoBot and freqtrade, it is not open source. Gunbot features different pricing strategies, but we’ll be focusing on “Ultimate”, which is its perpetual license plan.
Gunbot allows for custom strategies, by either using a configuration method accessible via web UI, or by using JavaScript. When using JavaScript, its syntax is a little less verbose than OctoBot and freqtrade, but still more complex than Naxbot’s strategy scripts:
// initialize customStratStore within pairLedger object
gb.data.pairLedger.customStratStore = gb.data.pairLedger.customStratStore || {};
// forced wait time reduces risk of double orders
function checkTime() {
return !gb.data.pairLedger.customStratStore.timeCheck ||
typeof gb.data.pairLedger.customStratStore.timeCheck !== 'number'
? ((gb.data.pairLedger.customStratStore.timeCheck = Date.now()), false)
: Date.now() - gb.data.pairLedger.customStratStore.timeCheck > 8000;
}
const enoughTimePassed = checkTime();
// set timestamp for checkTime in next round
const setTimestamp = () => (gb.data.pairLedger.customStratStore.timeCheck = Date.now());
// calculate RSI
function calculateRSI(period) {
const closes = gb.data.candlesClose.slice(-period);
const changes = closes.map((close, i) => (i === 0 ? 0 : close - closes[i - 1]));
const gains = changes.map((change) => (change > 0 ? change : 0));
const losses = changes.map((change) => (change < 0 ? Math.abs(change) : 0));
const avgGain = gains.reduce((sum, gain) => sum + gain, 0) / period;
const avgLoss = losses.reduce((sum, loss) => sum + loss, 0) / period;
const RS = avgGain / avgLoss;
const RSI = 100 - 100 / (1 + RS);
return RSI;
}
// calculate moving average
function calculateMA(period, data) {
const values = data.slice(-period);
const sum = values.reduce((total, value) => total + value, 0);
const MA = sum / period;
return MA;
}
// calculate Bollinger Bands
function calculateBB(period, stdDev, data) {
const values = data.slice(-period);
const MA = calculateMA(period, data);
const variance = values.reduce((total, value) => total + Math.pow(value - MA, 2), 0) / period;
const stdDeviation = Math.sqrt(variance);
const upperBand = MA + stdDev * stdDeviation;
const lowerBand = MA - stdDev * stdDeviation;
return { upperBand, lowerBand };
}
// calculate indicators
const RSI = calculateRSI(14);
const { upperBand, lowerBand } = calculateBB(20, 2, gb.data.candlesClose);
// log indicators and trading conditions
console.log(`RSI: ${RSI}`);
console.log(`Upper Bollinger Band: ${upperBand}`);
console.log(`Lower Bollinger Band: ${lowerBand}`);
console.log(`Got Bag: ${gb.data.gotBag}`);
// set buy and sell conditions
const buyConditions = RSI < 30 && !gb.data.gotBag;
const sellConditions = RSI > 70 && gb.data.gotBag;
if (enoughTimePassed) {
if (buyConditions) {
const buyAmount = gb.data.baseBalance * 0.95;
gb.method.buyMarket(buyAmount, gb.data.pairName);
setTimestamp();
} else if (sellConditions) {
gb.method.sellMarket(gb.data.quoteBalance, gb.data.pairName);
setTimestamp();
}
}
// Code is machine generated, review it and run in simulator mode firstGunbot’s licensing model is API-key based, meaning that whenever you change your credentials or want to start trading on a new exchange, you’ll have to get your key whitelisted first. This can be a bit cumbersome if you are planning on trading lots of exchanges.
Naxbot on the other hand uses on-chain licensing on the Solana blockchain, using your Solana wallet as proof of purchase upon login to the web interface. This also eliminates the risk of licensing servers being down / under maintenance and locking you out of the platform, as the Solana blockchain has a record of having over 99.9% uptime.
Like OctoBot and freqtrade, Gunbot also makes use of the ccxt library to allow its users to trade on lots of
exchanges. Its backend is JavaScript, meaning it runs less performant than Naxbot, and is restricted to run on a single
CPU core, whereas Naxbot can make use of all of them.
Feature Support Matrix
| Feature | Naxbot | Gunbot |
|---|---|---|
| Custom Strategies | (Lua) | (Configurations & JavaScript) |
| Automatic Strategy Optimizing | ||
| Licensing / DRM | On-Chain (Solana) | API-Key Based |
| Backend | Rust (native) | JavaScript |
| Multi-Threading | ||
| Backtesting | ||
| Visual Indicator Plotting | ||
| Web UI | ||
| Supported Exchanges | ccxt, extensible via custom modules | ccxt |
Frequently Asked Questions
How big of a difference does a “native” backend make?
We’ve benchmarked the same strategy programmed in Naxbot’s Lua implementation and in JavaScript:
function process(fast_length, slow_length)
fast_length = fast_length or 5
slow_length = slow_length or 17
local fast_rsi = rsi(close, fast_length)
local slow_rsi = rsi(close, slow_length)
local divergence = fast_rsi - slow_rsi
local long_entry_condition = crossover(divergence, 0)
local short_entry_condition = crossunder(divergence, 0)
-- again we declare a variable to avoid
-- multiple future calculations
local distance = atr(21)
-- we now calculate both long & short stop loss,
-- and then later decide which one to use based
-- on whether the signal is long or not.
local stop_loss_distance = distance * 1.5
local long_stop_loss = close - stop_loss_distance
local short_stop_loss = close + stop_loss_distance
local stop_loss = lif(long_entry_condition, long_stop_loss, short_stop_loss)
-- we do the same for our take profit target
local tp_1_distance = distance * 3
local long_tp_1 = close + tp_1_distance
local short_tp_1 = close - tp_1_distance
local tp_1 = lif(long_entry_condition, long_tp_1, short_tp_1)
local lll = ln(close)
local pp = percentrank(close, 10)
local cc = change(close, 1)
return {
long_entry_condition = long_entry_condition,
short_entry_condition = short_entry_condition,
long_exit_condition = constant(false),
short_exit_condition = constant(false),
stop_loss = stop_loss,
tp_1 = tp_1,
fast_rsi = fast_rsi,
slow_rsi = slow_rsi,
divergence = divergence,
}
end// Note that we have to re-implement all relevant indicators here, as we do not have access
// to the Rust backend from JavaScript.
function mkFakeClose() {
const close = [];
for (let i = 0; i < 57000; i++) {
close.push(Math.random() * 12 + 1000);
}
return close;
}
function divideArrays(a, b) {
return a.map((v, i) => v / b[i]);
}
function sma(source, length) {
const out = [];
for (let i = 0; i < source.length; i++) {
let previousSum = 0;
for (let j = Math.max(0, i - (length - 1)); j <= i; j++) {
previousSum += source[j];
}
out.push(previousSum / length);
}
return out;
}
function rma(source, length) {
const out = [];
for (let i = 0; i < source.length; i++) {
if (i <= 0) {
out.push(...sma([source[i]], length));
} else {
out.push((out[i - 1] * (length - 1) + source[i]) / length);
}
}
return out;
}
function crossover(a, b, prev_a, prev_b) {
return a > b && prev_a < prev_b;
}
function crossunder(a, b, prev_a, prev_b) {
return a < b && prev_a > prev_b;
}
function crossoverArrays(a, b) {
const out = [0];
for (let i = 1; i < a.length; i++) {
out.push(crossover(a[i], b[i], a[i - 1], b[i - 1]));
}
return out;
}
function crossunderArrays(a, b) {
const out = [0];
for (let i = 1; i < a.length; i++) {
out.push(crossunder(a[i], b[i], a[i - 1], b[i - 1]));
}
return out;
}
function rsi(source, length) {
const upwardChanges = [];
const downwardChanges = [];
upwardChanges.push(0);
downwardChanges.push(0);
for (let i = 1; i < source.length; i++) {
upwardChanges.push(Math.max(0, source[i] - source[i - 1]));
downwardChanges.push(Math.max(0, source[i - 1] - source[i]));
}
const dvec = divideArrays(rma(upwardChanges, length), rma(downwardChanges, length));
const out = dvec.map((v) => 100 - 100 / (1 + v));
for (let i = 0; i < length; i++) {
out[i] = 0;
}
return out;
}
function trueRange(high, low, prevClose) {
return Math.max(Math.max(high - low, Math.abs(high - prevClose)), Math.abs(low - prevClose));
}
function atr(highArray, lowArray, closeArray, length) {
const tr = highArray.map((v, i) => {
if (i <= 0) {
let sum = 0;
for (let j = 1; j <= length; j++) {
sum += trueRange(v, lowArray[j], closeArray[j - 1]) / length;
}
return sum;
} else {
return trueRange(v, lowArray[i], closeArray[i - 1]);
}
});
return rma(tr, length);
}
function percentrank(source, length) {
return source.map((v, i) => {
let numLeq = 0;
let startIdx = Math.max(0, i - length);
for (let j = startIdx; j < i; j++) {
if (source[j] <= v) {
numLeq++;
}
}
numLeq /= i - startIdx;
return numLeq * 100;
});
}
function change(source, length) {
return source.map((v, i) => {
const cmp = Math.max(0, i - length);
return v - source[cmp];
});
}
function stratBench(close, low, high) {
const fastRsi = rsi(close, 5);
const slowRsi = rsi(close, 17);
const divergence = fastRsi.map((v, i) => v - slowRsi[i]);
const longEntryCondition = crossoverArrays(
divergence,
divergence.map((v) => 0),
);
const shortEntryCondition = crossunderArrays(
divergence,
divergence.map((v) => 0),
);
const distance = atr(high, low, close, 17);
const stopLossDistance = distance.map((v) => v * 1.5);
const longStopLoss = close.map((v, i) => v - stopLossDistance[i]);
const shortStopLoss = close.map((v, i) => v + stopLossDistance[i]);
const stopLoss = longEntryCondition.map((v, i) => (v ? longStopLoss[i] : shortStopLoss[i]));
const tp1Distance = distance.map((v) => v * 3);
const longTp1 = close.map((v, i) => v + tp1Distance[i]);
const shortTp1 = close.map((v, i) => v - tp1Distance[i]);
const tp1 = longEntryCondition.map((v, i) => (v ? longTp1[i] : shortTp1[i]));
const ln = close.map((v) => Math.log(v));
const pp = percentrank(close, 10);
const cc = change(close, 1);
return {
longEntryCondition,
shortEntryCondition,
stopLoss,
tp1,
fastRsi,
slowRsi,
divergence,
longStopLoss,
};
}
for (let i = 0; i < 100; i++) {
const close = mkFakeClose();
const high = close.map((v) => v + Math.random() * 100);
const low = close.map((v) => v - Math.random() * 100);
const hrTime = process.hrtime();
const timeMicrosNow = hrTime[0] * 1000000 + hrTime[1] / 1000;
const results = stratBench(close, low, high);
const timeAfter = process.hrtime();
const timeMicrosAfter = timeAfter[0] * 1000000 + timeAfter[1] / 1000;
const timeTaken = timeMicrosAfter - timeMicrosNow;
console.log(`Time taken: ${timeTaken} us`);
}Both strategies have been run on 57,000 klines worth of fake data. Why 57,000? At the time of writing, this was how many klines we were able to fetch from the Binance BTC/USDT chart on the 1 minute timeframe. By now, I’m sure there are more!
The JavaScript version on average took between 40ms and 50ms to run for every iteration, with very few outliers. The Lua version using Naxbot’s backend was around 10x faster, with average iteration times between 3ms and 5ms.
Although we didn’t explicitly benchmark Python, using it without any libraries will probably yield
similar-to-slightly-worse results compared to JavaScript, while using it in conjunction with numpy should provide
considerably better results, which might even be on par with Naxbot (single-threaded only however).
Disclaimer
All strategy samples have been taken from the respective bots’ documentation.