Skip to main content

Custom strategy code examples

Coding a strategy can be as difficult as you want it to be.
Even with very basic JavaScript code it's possible to create complex strategies.


danger

Some examples can lead to immediate large trades. Use them to learn, not to trade.


Examples to learn from

The following examples show a few possible ways to code simple strategies. They are silly in functionality, and are not usable for actual trading. Each next example requires a bit more JavaScript knowledge.

Example 1

  • Trade USDT-BTC
  • Invest 100 USDT per trade, allow max 1 buy order
  • Market buy when price is below lowBB and fastSma is below ema1 (using readily available indicator data)
  • After the buy order fills, place a sell order 1% above the break even price
  • Do nothing until this sell order fills
// check if there are any open orders for this pair, 
// wait until they get filled before doing anything else
if (gb.data.openOrders.length > 0){
// print a message to the console logs
console.log('There is an open order, waiting...')
// stop doing things
return
}


// balance "settings": invest 100 USDT, convert into quote amount
// needed for calling the buy method
var baseAmount = 100
var buyAmount = baseAmount / gb.data.bid


// check if no bag is held yet AND price,
// lowBB and moving average criteria are true
if (
!gb.data.gotBag &&
gb.data.bid < gb.data.lowBB &&
gb.data.fastSma < gb.data.ema1
){
// conditions are true, fire a market buy order
// using the amount calculated in buyAmount
gb.method.buyMarket(buyAmount, gb.data.pairName)
}
// place a limit sell order if we have a bag
else if ( gb.data.gotBag ) {
// fire the sell order, pass in the amount, price and pair name.
// price is calculated as breakEven * 1.01
gb.method.sellLimit(gb.data.quoteBalance, gb.data.breakEven * 1.01, gb.data.pairName)
}


Example 2

  • Keep the basic workings from example 1
  • Additionally calculate a Hull Moving Average (HMA) using the last 50 candle close prices as input
  • Place the sell order 1% above bid price, at the moment bid goes above HMA
// check if there are any open orders for this pair, 
// wait until they get filled before doing anything else
if (gb.data.openOrders.length > 0) {
// print a message to the console logs
console.log('There is an open order, waiting...')
// stop doing things
return
}


// balance "settings": invest 100 USDT, convert into
// quote amount needed for calling the buy method
var baseAmount = 100
var buyAmount = baseAmount / gb.data.bid

// define a variable to save the Hull Moving Average to
var hma

// get indicator data from tulind
// use candlesClose data as input, calculate a 50 period Hull Moving Average
gb.method.tulind.indicators.hma.indicator([gb.data.candlesClose], [50], function (err, results) {

// tulind returns an array with hma values,
// assign the most recent value to the hma variable
// notation is a bit awkward, the ".length -1" part
// makes it select the very last value
hma = results[0][results[0].length - 1]
});


// check if no bag is held yet AND price,
// lowBB and moving average criteria are true
if (
!gb.data.gotBag &&
gb.data.bid < gb.data.lowBB &&
gb.data.fastSma < gb.data.ema1
) {
// conditions are true, fire a market buy order
// using the amount calculated in buyAmount
gb.method.buyMarket(buyAmount, gb.data.pairName)
}
// place a limit sell order if we have a bag
// make sure the sell method is only called when bid > hma
else if (gb.data.gotBag && gb.data.bid > hma) {
// fire the sell order, pass in the amount, price and pair name.
// price is calculated as bid * 1.01
gb.method.sellLimit(gb.data.quoteBalance, gb.data.bid * 1.01, gb.data.pairName)
}


Example 3

  • Trade USDT-BTC
  • Invest 100 USDT per trade, allow max 1 buy order
  • Code a bit more readable, assigning variables for everything used in conditionals
  • Buy when a 50 period ema has crossed over a 200 period ema, AND within at most 2 candles later the candle open price is above ema1
  • Place a sell order 5% above break even
// check if there are any open orders for this pair, 
//wait until they get filled before doing anything else
if (gb.data.openOrders.length > 0) {
// print a message to the console logs
console.log('There is an open order, waiting...')
// stop doing things
return
}


// balance "settings": invest 100 USDT, convert into
// quote amount needed for calling the buy method
var baseAmount = 100
var buyAmount = baseAmount / gb.data.bid


// needed variables for conditional logic
// some are left undefined, to set them later when the values can be calculated
// emaCrossup is intentionally set false,
// only to be set true when a recent crossup is detected
var gotBag = gb.data.gotBag
var bid = gb.data.bid
var ema50
var ema200
var emaCrossup = false


// get indicator data from tulind
// use candlesClose data as input, calculate a 50
// and 200 period Exponential Moving Average
gb.method.tulind.indicators.ema.indicator([gb.data.candlesClose], [50], function (err, results) {

// tulind returns an array with ema value,
// save full array to be able to look for crosses later
ema50 = results[0]
});

gb.method.tulind.indicators.ema.indicator([gb.data.candlesClose], [200], function (err, results) {

// tulind returns an array with ema value,
// save full array to be able to look for crosses later
ema200 = results[0]
});


// figure out if there was a crossover of ema50 > ema200 in the last two candles
// do this in two conditionals, once for the most recent candle
// and once for the previous candle
// if true, save the result in the emaCrossup variable
if (
ema50[ema50.length - 1] > ema200[ema200.length - 1] &&
ema50[ema50.length - 2] < ema200[ema200.length - 2]
) {
emaCrossup = true
}
else if (
ema50[ema50.length - 2] > ema200[ema200.length - 2] &&
ema50[ema50.length - 3] < ema200[ema200.length - 3]
) {
emaCrossup = true
}


// buy if there is no bag, and there was a recent ema crossup
// and bid price is above ema50
if (!gotBag && bid > ema50[ema50.length - 1] && emaCrossup) {
// conditions are true, fire a market buy order using the amount calculated in buyAmount
gb.method.buyMarket(buyAmount, gb.data.pairName)
}
// place a limit sell order if we have a bag
// make sure the sell method is only called when
else if (gotBag) {
// fire the sell order, pass in the amount, price and pair name. Price is calculated as bid * 1.01
gb.method.sellLimit(gb.data.quoteBalance, gb.data.breakEven * 1.05, gb.data.pairName)
}


Example 4

  • Trade USDT-BTC and USDT-XRP
  • Assume you already have BTC and XRP
  • Sell 1 BTC when news about BTC tagged "bearish" appears on cryptopanic.com, containing the keyword "Korea", then place a buy order 5% below the current price
  • Sell 10000 XRP whenever news about XRP tagged "bearish" appears with the keyword "sec"
  • Do all of this in a single strategy running on a single pair in Gunbot
// require the Cryptopanic module
// install https://www.npmjs.com/package/cryptopanic in an outside folder,
// then copy the modules to the 'user_modules' folder inside the Gunbot root folder
const _ = gb.method.require(gb.modulesPath + '/cryptopanic')

// make a connection to Cryptopanic
const cp = new Cryptopanic({ auth_token: '<API_TOKEN>' })

// creata variables to store status of detected FUD
let koreaFud
let secFud

// get data from Cryptopanic and do a simple check if any of the BTC news includes 'Korea', and if there is XRP news that included 'SEC'
// this is not a complete solution, for example it does not consider if news is old or has already been traded
cp.currencies(['BTC', 'XRP'])
.filter('bearish')
.fetchPosts()
.then((articles) => {

// loop trough articles, search if specific strings exist and save result
articles.forEach(article => {
// convert article object to string to easily search its contents
const articleString = JSON.stringify(article)

if (articleString.indexOf('Korea') > -1 && articleString.indexOf('BTC') > -1) {
koreaFud = true
}

if (articleString.indexOf('SEC') > -1 && articleString.indexOf('XRP') > -1) {
secFud = true
}
});

// fire BTC orders if Korea FUD detected
if (koreaFud) {
// first sell 1 BTC in a market order
gb.method.sellMarket(1, 'USDT-BTC')
// wait for the promise to resolve, then check if the order filled before placing a limit buy 5% below current price
.then((result) => {
if (result.info.status === 'FILLED') {
gb.method.buyLimit(1, gb.data.bid * 0.95, 'USDT-BTC')
}
})
}

// sell XRP if SEC FUD detected
if (secFud) {
gb.method.sellMarket(10000, 'USDT-XRP')
}

})

Example 5

  • Trade USDT-BTC
  • Invest 100 USDT per trade, allow max 1 buy order
  • Strategy itself uses 3m candles, additionally fetch 60m candles
  • Only collect data and allow trades in first minute of the hour
  • Calculate TD Sequential using an external module, transform input data to the needed format
  • Place buy order when bid price > ema1, using 3m data AND TD Sequential on hourly chart has a buy setup index of 8 or higher
  • Sell when TD sequential has a sell setup index of at least 11 and price is above break even
  • Use the lodash module to simplify some code
// strategy uses two external modules: 'lodash' and 'tdsequential'
// install them with npm in an outside folder, and copy the modules to the 'user_modules' folder inside the Gunbot root folder
const _ = gb.method.require(gb.modulesPath + '/lodash')
const TDSequential = gb.method.require(gb.modulesPath + '/tdsequential')

// don't do anything unless current minute is first of the hour
const currentMinute = new Date(Date.now()).getMinutes();
if (currentMinute != 0) {
return
}

// get 300 60m candles, and persistently save them
// to Gunbot pair ledger - for no real reason except showing that you can do this
if (_.isNil(gb.data.pairLedger.customStratStore)){
// create custom strategy store
gb.data.pairLedger.customStratStore = {}
}
gb.data.pairLedger.customStratStore.myCandles = await gb.method.getCandles(300, 60, gb.data.pairName)

// balance "settings": invest 100 USDT, convert into quote amount needed for calling the buy method
const baseAmount = 100
const buyAmount = baseAmount / gb.data.bid

// to calculate TD Sequential, first transform the collected candle data to the format required by this module
let candles_60m_transformed = []
gb.data.pairLedger.customStratStore.myCandles.close.forEach(function (item, index) {
let temp = {}
temp.time = item.timestamp
temp.close = item.value
temp.high = gb.data.pairLedger.customStratStore.myCandles.high[index].value
temp.low = gb.data.pairLedger.customStratStore.myCandles.low[index].value
temp.open = gb.data.pairLedger.customStratStore.myCandles.open[index].value
temp.volume = gb.data.pairLedger.customStratStore.myCandles.volume[index].value
candles_60m_transformed.push(temp)
});

// calculate TD Sequential and only use the most recent value
const tdSequentialData = _.last(TDSequential(candles_60m_transformed))

// define trading conditions
const buyConditions = tdSequentialData.buySetupIndex >= 8 && gb.data.bid > gb.data.ema1 && !gb.data.gotBag
const sellConditions = tdSequentialData.sellSetupIndex >= 11 && gb.data.bid > gb.data.breakEven && gb.data.gotBag

// place orders if conditions are true
if (buyConditions) {
gb.method.buyMarket(buyAmount, gb.data.pairName)
}
else if (sellConditions) {
gb.method.sellMarket(gb.data.quoteBalance, gb.data.pairName)
}

Strategy examples

These examples can provide a foundation for your own trading strategies, but it's important to note that each strategy will require careful testing before implementation.

It's worth mentioning that the examples do not incorporate checks to ensure referenced data is defined prior to executing orders, which may be necessary in some cases. Additionally, incorporating error handling into your code can be beneficial, particularly if you're working with different trading methods that have a lower chance of success than standard market orders.

Keltner crossover

// require external modules
const kc = gb.method.require(gb.modulesPath + '/keltnerchannel').kc
const _ = gb.method.require(gb.modulesPath + '/lodash')

// forced short wait time between runs that do something
// mostly to reduce risk of double orders in case the exchange doesn't update balances immediately
let enoughTimePassed = false
if (_.isNil(gb.data.pairLedger.customStratStore)) {
gb.data.pairLedger.customStratStore = {}

if (_.isNil(gb.data.pairLedger.customStratStore.timeCheck)) {
gb.data.pairLedger.customStratStore.timeCheck = Date.now()
}
else {
if (Date.now() - gb.data.pairLedger.customStratStore.timeCheck > 8000) {
enoughTimePassed = true
}
}
}

const setTimestamp = function () {
gb.data.pairLedger.customStratStore.timeCheck = Date.now()
}

if (enoughTimePassed) {

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// collect indicator data

let candlesReformatted = []

gb.data.candlesClose.forEach(function (item, index) {
let temp = {}
temp.close = item
temp.high = gb.data.candlesHigh[index]
temp.low = gb.data.candlesLow[index]
candlesReformatted.push(temp)
});

const keltnerChannel = kc(candlesReformatted, 20, 1, true)
const lowestPriceLast10Candles = Math.min(...gb.data.candlesLow.slice(-10))
const macd = gb.data.macd
const macdSignal = gb.data.macdSignal
let ema200
let obv
let obvMa21

gb.method.tulind.indicators.ema.indicator([gb.data.candlesClose], [200], function (err, results) {
ema200 = results[0]
});

gb.method.tulind.indicators.obv.indicator([gb.data.candlesClose, gb.data.candlesVolume], [], function (err, results) {
obv = results[0]
});

gb.method.tulind.indicators.sma.indicator([obv], [21], function (err, results) {
obvMa21 = results[0]
});


////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// trading conditions

const buyAmount = parseFloat(gb.data.pairLedger.whatstrat.TRADING_LIMIT) / gb.data.bid
const sellTarget = gb.data.breakEven + ((gb.data.breakEven - lowestPriceLast10Candles) * 1.5)
const openSellRate = gb.data.openOrders?.[0]?.rate
const stopTarget = openSellRate - (openSellRate - gb.data.breakEven) - ((openSellRate - gb.data.breakEven) * 0.67)

const entryConditions = (
!gb.data.gotBag &&
// prev candle high > upper keltner
gb.data.candlesHigh[gb.gb.data.candlesHigh.length - 2] > keltnerChannel.upper[keltnerChannel.upper.length - 2] &&
// prev full candle > ema 200
gb.data.candlesOpen[gb.data.candlesOpen.length - 2] > ema200[ema200.length - 2] &&
gb.data.candlesHigh[gb.data.candlesHigh.length - 2] > ema200[ema200.length - 2] &&
gb.data.candlesLow[gb.data.candlesLow.length - 2] > ema200[ema200.length - 2] &&
gb.data.candlesClose[gb.data.candlesClose.length - 2] > ema200[ema200.length - 2] &&
// candle open > upper keltner
gb.data.candlesOpen[gb.data.candlesOpen.length - 1] > keltnerChannel.upper[keltnerChannel.upper.length - 1] &&
// obv ma 21 > obv
obvMa21[obvMa21.length - 1] > obv[obv.length - 1] &&
// macd > signal
macd > macdSignal
)

const sellConditions = (
gb.data.gotBag &&
gb.data.openOrders.length === 0
)

const stopConditions = (
gb.data.gotBag &&
gb.data.openOrders.length > 0 &&
gb.data.bid < stopTarget
)

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// order handling

if (entryConditions) {
gb.method.buyMarket(buyAmount, gb.data.pairName)
setTimestamp()
}
else if (sellConditions) {
gb.method.sellLimit(gb.data.quoteBalance, sellTarget, gb.data.pairName)
setTimestamp()
}
else if (stopConditions) {
gb.method.sellMarket(gb.data.quoteBalance, gb.data.pairName)
setTimestamp()
}

}


Dual RSI

// require external modules
const _ = gb.method.require(gb.modulesPath + '/lodash')
// forced short wait time between runs that do something
// mostly to reduce risk of double orders in case the exchange doesn't update balances immediately
let enoughTimePassed = false
if (_.isNil(gb.data.pairLedger.customStratStore)) {
gb.data.pairLedger.customStratStore = {}

if (_.isNil(gb.data.pairLedger.customStratStore.timeCheck)) {
gb.data.pairLedger.customStratStore.timeCheck = Date.now()
}
else {
if (Date.now() - gb.data.pairLedger.customStratStore.timeCheck > 8000) {
enoughTimePassed = true
}
}
}

const setTimestamp = function () {
gb.data.pairLedger.customStratStore.timeCheck = Date.now()
}

if (enoughTimePassed) {

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// collect indicator data

let rsi10
let rsi20

gb.method.tulind.indicators.rsi.indicator([gb.data.candlesClose], [10], function (err, results) {
rsi10 = results[0]
});

gb.method.tulind.indicators.rsi.indicator([gb.data.candlesClose], [20], function (err, results) {
rsi20 = results[0]
});


////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// trading conditions

const entryConditions = (
rsi10[rsi10.length - 1] > rsi20[rsi20.length - 1] &&
rsi10[rsi10.length - 2] < rsi20[rsi20.length - 2]
)

const exitConditions = (
gb.data.gotBag &&
rsi10[rsi10.length - 1] < rsi20[rsi20.length - 1] &&
rsi10[rsi10.length - 2] > rsi20[rsi20.length - 2]
)


////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// order handling

const buyAmount = parseFloat(gb.data.pairLedger.whatstrat.TRADING_LIMIT) / gb.data.bid

if (entryConditions) {
gb.method.buyMarket(buyAmount, gb.data.pairName)
setTimestamp()
}
else if (exitConditions) {
gb.method.sellMarket(gb.data.quoteBalance, gb.data.pairName)
setTimestamp()
}

}

Trail price after stochRsi target

// require external modules
const _ = gb.method.require(gb.modulesPath + '/lodash')

// forced short wait time between runs that do something
// mostly to reduce risk of double orders in case the exchange doesn't update balances immediately
let enoughTimePassed = false
if (_.isNil(gb.data.pairLedger.customStratStore)) {
gb.data.pairLedger.customStratStore = {}

if (_.isNil(gb.data.pairLedger.customStratStore.timeCheck)) {
gb.data.pairLedger.customStratStore.timeCheck = Date.now()
}
else {
if (Date.now() - gb.data.pairLedger.customStratStore.timeCheck > 8000) {
enoughTimePassed = true
}
}
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// functions used in strategy

const setTimestamp = function () {
gb.data.pairLedger.customStratStore.timeCheck = Date.now()
}

const clearTrailingTargets = function () {

if (!_.isNil(gb.data.pairLedger.customStratStore.sellTrailingTarget)) {
delete gb.data.pairLedger.customStratStore.sellTrailingTarget
}

if (!_.isNil(gb.data.pairLedger.customStratStore.buyTrailingTarget)) {
delete gb.data.pairLedger.customStratStore.buyTrailingTarget
}
}

if (enoughTimePassed) {

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// start price trailing after hitting stochRsi target

const buyAmount = parseFloat(gb.data.pairLedger.whatstrat.TRADING_LIMIT) / gb.data.bid

// handle buy trailing, start trailing after stochRsi target hits
if (_.isNil(gb.data.pairLedger.customStratStore.buyTrailingTarget) && gb.data.stochRsi < 0.1 && !gb.data.gotBag) {
// set initial trailing stop
gb.data.pairLedger.customStratStore.buyTrailingTarget = gb.data.ask * 1.005
}
else if (!_.isNil(gb.data.pairLedger.customStratStore.buyTrailingTarget) && gb.data.pairLedger.customStratStore.buyTrailingTarget > gb.data.ask * 1.005) {
// update trailing stop
gb.data.pairLedger.customStratStore.buyTrailingTarget = gb.data.ask * 1.005
}

// handle sell trailing, start trailing after stochRsi target hits
if (_.isNil(gb.data.pairLedger.customStratStore.sellTrailingTarget) && gb.data.stochRsi > 0.9 && gb.data.gotBag) {
// set initial trailing stop
gb.data.pairLedger.customStratStore.sellTrailingTarget = gb.data.bid * 0.995
}
else if (!_.isNil(gb.data.pairLedger.customStratStore.sellTrailingTarget) && gb.data.pairLedger.customStratStore.sellTrailingTarget < gb.data.bid * 0.995) {
// update trailing stop
gb.data.pairLedger.customStratStore.sellTrailingTarget = gb.data.bid * 0.995
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// fire orders when hitting a trailing stop, remove trailing stop after placing order

if (!_.isNil(gb.data.pairLedger.customStratStore.buyTrailingTarget) ?
gb.data.ask > gb.data.pairLedger.customStratStore.buyTrailingTarget :
false) {

gb.method.buyMarket(buyAmount, gb.data.pairName)
clearTrailingTargets()
setTimestamp()
}
else if (!_.isNil(gb.data.pairLedger.customStratStore.sellTrailingTarget) ?
gb.data.bid < gb.data.pairLedger.customStratStore.sellTrailingTarget :
false) {

gb.method.sellMarket(gb.data.quoteBalance, gb.data.pairName)
clearTrailingTargets()
setTimestamp()

}
}