Skip to main content

Custom Strategy Code Examples

Coding a custom strategy in Gunbot can be as simple or as complex as you desire. Even with very basic JavaScript knowledge (covering functions, conditional statements like if...else, comparison operators, and declarations), it's possible to create sophisticated trading strategies.


Important Note

The examples provided below are for educational purposes only and may lead to immediate and potentially large trades if used directly. Use them to understand concepts and learn, not for live trading without thorough testing and adaptation.


Examples to Learn From​

The following examples demonstrate various ways to code simple strategies. They are intentionally simplistic in functionality and are not suitable for actual trading. Each subsequent example generally requires a bit more JavaScript knowledge.

Example 1: Basic Buy Low, Sell High​

  • Pair: USDT-BTC
  • Investment: 100 USDT per trade, maximum 1 buy order active.
  • Buy Condition: Market buy when the current price is below the lower Bollinger Band (lowBB) and the fast Simple Moving Average (fastSma) is below Exponential Moving Average 1 (ema1), using readily available indicator data.
  • Sell Condition: After a buy order fills, place a limit sell order 1% above the break-even price.
  • Logic: Do nothing until the 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 further execution for this cycle.
return;
}

// Balance "settings": invest 100 USDT.
// Convert this base amount into the quote amount needed for calling the buy method.
var baseAmount = 100;
var buyAmount = baseAmount / gb.data.bid; // Using current bid price for amount calculation.

// Check if no bag (assets) 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 (entire quote balance),
// target price, and pair name.
// Price is calculated as breakEven * 1.01 (1% profit).
gb.method.sellLimit(gb.data.quoteBalance, gb.data.breakEven * 1.01, gb.data.pairName);
}

Example 2: Adding Custom Indicator (HMA)​

  • Keeps the basic workings from Example 1.
  • Additionally calculates a Hull Moving Average (HMA) using the last 50 candle close prices as input via tulind.
  • Sell order is placed 1% above the current bid price, at the moment the bid price goes above the calculated HMA.
// Check for open orders, same as Example 1.
if (gb.data.openOrders.length > 0) {
console.log('There is an open order, waiting...');
return;
}

// Investment settings, same as Example 1.
var baseAmount = 100;
var buyAmount = baseAmount / gb.data.bid;

// Define a variable to store the Hull Moving Average.
var hma;

// Get indicator data from tulind.
// Use gb.data.candlesClose data as input, calculate a 50-period Hull Moving Average.
gb.method.tulind.indicators.hma.indicator([gb.data.candlesClose], [50], function (err, results) {
if (err) {
console.error('Error calculating HMA:', err);
return;
}
// Tulind returns an array of HMA values.
// Assign the most recent value to the hma variable.
// The notation results[0].length - 1 selects the very last value from the first result array.
hma = results[0][results[0].length - 1];
});

// Ensure HMA is calculated before proceeding with logic that uses it.
if (typeof hma === 'undefined') {
console.log('HMA not calculated yet, skipping trade logic.');
return;
}

// Buy conditions, same as Example 1.
if (
!gb.data.gotBag &&
gb.data.bid < gb.data.lowBB &&
gb.data.fastSma < gb.data.ema1
) {
gb.method.buyMarket(buyAmount, gb.data.pairName);
}
// Sell condition: if a bag is held and current bid is above HMA.
else if (gb.data.gotBag && gb.data.bid > hma) {
// Fire the sell order. Price is calculated as current bid * 1.01.
gb.method.sellLimit(gb.data.quoteBalance, gb.data.bid * 1.01, gb.data.pairName);
}

Example 3: EMA Crossover Strategy​

  • Pair: USDT-BTC
  • Investment: 100 USDT per trade, max 1 buy order.
  • Readability: Code is structured with more variables for clarity in conditionals.
  • Buy Condition: 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.
  • Sell Condition: Place a sell order 5% above the break-even price.
// Check for open orders.
if (gb.data.openOrders.length > 0) {
console.log('There is an open order, waiting...');
return;
}

// Investment settings.
var baseAmount = 100;
var buyAmount = baseAmount / gb.data.bid;

// Variables for conditional logic.
// Some are left undefined to be set later when values can be calculated.
// emaCrossup is intentionally set to false, only set to true if a recent crossup is detected.
var gotBag = gb.data.gotBag;
var bid = gb.data.bid;
var ema50Values; // Will store array of EMA50
var ema200Values; // Will store array of EMA200
var emaCrossup = false;

// Get indicator data from tulind.
// Use gb.data.candlesClose data as input, calculate 50 and 200 period Exponential Moving Averages.
gb.method.tulind.indicators.ema.indicator([gb.data.candlesClose], [50], function (err, results) {
if (err) { console.error('Error calculating EMA50:', err); return; }
ema50Values = results[0]; // Save the full array to look for crosses.
});

gb.method.tulind.indicators.ema.indicator([gb.data.candlesClose], [200], function (err, results) {
if (err) { console.error('Error calculating EMA200:', err); return; }
ema200Values = results[0]; // Save the full array.
});

// Ensure EMAs are calculated.
if (!ema50Values || !ema200Values || ema50Values.length < 3 || ema200Values.length < 3) { // Need at least 3 for lookback
console.log('EMA values not sufficiently populated yet.');
return;
}

// Figure out if there was a crossover of EMA50 > EMA200 in the last two candles.
// Check current candle forming (index length-1) vs previous (length-2)
// and previous (length-2) vs one before (length-3)
if (
ema50Values[ema50Values.length - 1] > ema200Values[ema200Values.length - 1] &&
ema50Values[ema50Values.length - 2] < ema200Values[ema200Values.length - 2]
) {
emaCrossup = true;
} else if (
ema50Values[ema50Values.length - 2] > ema200Values[ema200Values.length - 2] &&
ema50Values[ema50Values.length - 3] < ema200Values[ema200Values.length - 3]
) {
emaCrossup = true;
}

// Buy if no bag, there was a recent EMA crossup, and bid price is above the latest EMA50.
// Also check if current candle open is above gb.data.ema1 (short-term EMA from Gunbot settings).
const currentCandleOpen = gb.data.candlesOpen[gb.data.candlesOpen.length -1];
if (!gotBag && emaCrossup && currentCandleOpen > gb.data.ema1 && bid > ema50Values[ema50Values.length - 1]) {
gb.method.buyMarket(buyAmount, gb.data.pairName);
}
// Place a limit sell order if a bag is held.
else if (gotBag) {
gb.method.sellLimit(gb.data.quoteBalance, gb.data.breakEven * 1.05, gb.data.pairName); // 5% profit target
}

Example 4: News-Based Trading with Cryptopanic​

  • Pairs: USDT-BTC and USDT-XRP.
  • Assumption: You already hold BTC and XRP.
  • BTC Logic: 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.
  • XRP Logic: Sell 10,000 XRP whenever news about XRP tagged "bearish" appears with the keyword "SEC".
  • Execution: All logic runs in a single strategy file (e.g., attached to USDT-BTC, but trades other pairs).
  • Note: This requires installing the cryptopanic npm package in the user_modules folder. Your Cryptopanic API token is needed.
// Require the Cryptopanic module.
// Install from https://www.npmjs.com/package/cryptopanic in an external folder,
// then copy the 'cryptopanic' folder from its 'node_modules' to the 'user_modules' folder inside the Gunbot root directory.
const Cryptopanic = gb.method.require(gb.modulesPath + '/cryptopanic');

// Make a connection to Cryptopanic using your API token.
const cp = new Cryptopanic({ auth_token: '<YOUR_API_TOKEN_HERE>' });

// Variables to store status of detected FUD (Fear, Uncertainty, Doubt).
let koreaFud = false;
let secFud = false;

// Get data from Cryptopanic.
// This is a simplified example; it doesn't consider if news is old or already traded upon.
cp.currencies(['BTC', 'XRP'])
.filter('bearish') // Filter for bearish news
.fetchPosts()
.then((articles) => {
// Loop through articles, search if specific strings exist and save the result.
articles.forEach(article => {
const articleString = JSON.stringify(article).toLowerCase(); // Convert to string and lower case for easier search.

if (articleString.includes('korea') && article.currencies && article.currencies.some(c => c.code === 'BTC')) {
koreaFud = true;
}

if (articleString.includes('sec') && article.currencies && article.currencies.some(c => c.code === 'XRP')) {
secFud = true;
}
});

// Fire BTC orders if Korea FUD is detected.
if (koreaFud && gb.data.pairName === 'USDT-BTC') { // Example: only act if current pair is USDT-BTC
// First, sell 1 BTC in a market order.
gb.method.sellMarket(1, 'USDT-BTC')
.then((result) => {
// Wait for the promise to resolve, then check if the order filled before placing a limit buy.
if (result && result.info && result.info.status === 'FILLED') {
console.log('BTC sold due to Korea FUD, placing buy order.');
gb.method.buyLimit(1, gb.data.bid * 0.95, 'USDT-BTC'); // Buy 5% below current bid
}
}).catch(err => console.error('Error selling BTC or placing buy limit:', err));
}

// Sell XRP if SEC FUD is detected.
if (secFud && gb.data.pairName === 'USDT-XRP') { // Example: only act if current pair is USDT-XRP
console.log('XRP SEC FUD detected, selling XRP.');
gb.method.sellMarket(10000, 'USDT-XRP')
.catch(err => console.error('Error selling XRP:', err));
}
}).catch(err => console.error('Error fetching Cryptopanic posts:', err));

Example 5: Multi-Timeframe Analysis with TD Sequential​

  • Pair: USDT-BTC
  • Investment: 100 USDT per trade, max 1 buy order.
  • Timeframes: Strategy uses 3-minute candles for primary logic but also fetches 60-minute candles for TD Sequential.
  • Execution Window: Only collect data and allow trades in the first minute of each hour.
  • Indicator: Calculate TD Sequential using an external module (tdsequential), transforming input data to the needed format.
  • Buy Condition: Place buy order when bid price > ema1 (on 3m data) AND TD Sequential on the hourly chart has a buy setup index of 8 or higher.
  • Sell Condition: Sell when TD Sequential (hourly) has a sell setup index of at least 11 and price is above break-even.
  • Modules: Uses lodash and tdsequential (install in user_modules).
// Strategy uses two external modules: 'lodash' and 'tdsequential'.
// Install them with npm in an outside folder (e.g., npm i lodash tdsequential),
// then copy their respective folders from 'node_modules' to the 'user_modules' folder inside the Gunbot root directory.
const _ = gb.method.require(gb.modulesPath + '/lodash');
const TDSequential = gb.method.require(gb.modulesPath + '/tdsequential');

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

async function strategyLogic() {
// Initialize customStratStore if it doesn't exist for persistent storage.
if (_.isNil(gb.data.pairLedger.customStratStore)) {
gb.data.pairLedger.customStratStore = {};
}

// Get 300 60-minute candles and persistently save them to Gunbot pair ledger.
// This is for demonstration; saving large candle sets frequently might not be optimal.
try {
gb.data.pairLedger.customStratStore.myCandles = await gb.method.getCandles(300, 60, gb.data.pairName);
} catch (err) {
console.error('Error fetching 60m candles:', err);
return;
}

const hourlyCandles = gb.data.pairLedger.customStratStore.myCandles;
if (!hourlyCandles || !hourlyCandles.close || hourlyCandles.close.length === 0) {
console.log('Hourly candle data not available.');
return;
}

// Investment settings.
const baseAmount = 100;
const buyAmount = baseAmount / gb.data.bid;

// To calculate TD Sequential, first transform the collected candle data to the format required by the module.
let candles_60m_transformed = [];
hourlyCandles.close.forEach(function (item, index) {
let temp = {};
// Ensure all properties exist before accessing .value or .timestamp
temp.time = hourlyCandles.timestamp[index] ? hourlyCandles.timestamp[index].value : undefined; // Original example had item.timestamp
temp.close = item ? item.value : undefined;
temp.high = hourlyCandles.high[index] ? hourlyCandles.high[index].value : undefined;
temp.low = hourlyCandles.low[index] ? hourlyCandles.low[index].value : undefined;
temp.open = hourlyCandles.open[index] ? hourlyCandles.open[index].value : undefined;
temp.volume = hourlyCandles.volume[index] ? hourlyCandles.volume[index].value : undefined;
// Only push if essential data (like close) is present
if (typeof temp.close !== 'undefined') {
candles_60m_transformed.push(temp);
}
});

if (candles_60m_transformed.length === 0) {
console.log('No valid 60m candles to process for TD Sequential.');
return;
}

// Calculate TD Sequential and only use the most recent value.
const tdSequentialDataAll = TDSequential(candles_60m_transformed);
if (!tdSequentialDataAll || tdSequentialDataAll.length === 0) {
console.log('TD Sequential calculation returned no data.');
return;
}
const tdSequentialData = _.last(tdSequentialDataAll);


// 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) {
console.log('TD Sequential Buy condition met.');
gb.method.buyMarket(buyAmount, gb.data.pairName);
} else if (sellConditions) {
console.log('TD Sequential Sell condition met.');
gb.method.sellMarket(gb.data.quoteBalance, gb.data.pairName);
}
}

// Call the async function to execute strategy logic
strategyLogic().catch(err => console.error("Error in strategy logic:", err));

Strategy Examples​

These examples can provide a foundation for your own trading strategies. However, it's crucial to note that each strategy will require careful testing and validation before implementation in a live trading environment.

It's also worth mentioning that these examples often do not incorporate comprehensive checks to ensure all referenced data (e.g., gb.data.ema1, gb.data.lowBB) is defined and available prior to executing orders. In real-world scenarios, adding such checks and robust error handling is highly recommended, particularly when working with different trading methods (like limit orders) that have a lower chance of immediate success compared to standard market orders.

Keltner Crossover​

// Require external modules: 'keltnerchannel' and 'lodash'.
// Install them (e.g., npm i keltnerchannel lodash) and copy to 'user_modules'.
const kc = gb.method.require(gb.modulesPath + '/keltnerchannel').kc;
const _ = gb.method.require(gb.modulesPath + '/lodash');

// Forced short wait time between runs that perform actions.
// This is mainly to reduce the risk of double orders if 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();
enoughTimePassed = true; // Allow action on first run
} else {
if (Date.now() - gb.data.pairLedger.customStratStore.timeCheck > 8000) { // 8 seconds
enoughTimePassed = true;
}
}

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

if (enoughTimePassed) {

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Collect indicator data

let candlesReformatted = [];
// Ensure gb.data.candlesClose and other candle arrays are populated
if (!gb.data.candlesClose || gb.data.candlesClose.length === 0) {
console.log('Candle data not available for Keltner Channel calculation.');
return;
}

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);
});

// Ensure candlesReformatted has enough data for Keltner Channel (e.g., period of 20)
if (candlesReformatted.length < 20) {
console.log('Not enough candle data for Keltner Channel calculation.');
return;
}
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) {
if (err) { console.error('Error calculating EMA200 for Keltner:', err); return; }
ema200 = results[0];
});

// Ensure gb.data.candlesVolume is populated for OBV
if (!gb.data.candlesVolume || gb.data.candlesVolume.length === 0) {
console.log('Volume data not available for OBV calculation.');
return;
}
gb.method.tulind.indicators.obv.indicator([gb.data.candlesClose, gb.data.candlesVolume], [], function (err, results) {
if (err) { console.error('Error calculating OBV for Keltner:', err); return; }
obv = results[0];
// Calculate SMA of OBV only after OBV is available
if (obv && obv.length > 0) {
gb.method.tulind.indicators.sma.indicator([obv], [21], function (errSma, resultsSma) {
if (errSma) { console.error('Error calculating OBV SMA for Keltner:', errSma); return; }
obvMa21 = resultsSma[0];
});
}
});

// Wait for async indicators to be calculated
if (typeof ema200 === 'undefined' || typeof obv === 'undefined' || typeof obvMa21 === 'undefined' || keltnerChannel.upper.length === 0) {
console.log('Keltner strategy indicators not ready yet.');
return;
}
// Check array lengths to prevent errors on insufficient data
if (gb.data.candlesHigh.length < 2 || keltnerChannel.upper.length < 2 || ema200.length < 2 || obvMa21.length < 1 || obv.length < 1) {
console.log('Insufficient data for Keltner strategy conditions.');
return;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Trading conditions

const tradingLimit = parseFloat(gb.data.pairLedger.whatstrat.TRADING_LIMIT);
if (isNaN(tradingLimit) || tradingLimit <=0) {
console.log('TRADING_LIMIT not valid for Keltner strategy.');
return;
}
const buyAmount = tradingLimit / gb.data.bid;
const sellTarget = gb.data.breakEven + ((gb.data.breakEven - lowestPriceLast10Candles) * 1.5);
const openSellRate = gb.data.openOrders && gb.data.openOrders[0] ? gb.data.openOrders[0].rate : null;
let stopTarget = null;
if (openSellRate && gb.data.breakEven) {
stopTarget = openSellRate - (openSellRate - gb.data.breakEven) - ((openSellRate - gb.data.breakEven) * 0.67);
}


const entryConditions = (
!gb.data.gotBag &&
// Previous candle high > upper Keltner
gb.data.candlesHigh[gb.data.candlesHigh.length - 2] > keltnerChannel.upper[keltnerChannel.upper.length - 2] &&
// Previous 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] &&
// Current 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 &&
!isNaN(sellTarget) // Ensure sellTarget is a valid number
);

const stopConditions = (
gb.data.gotBag &&
gb.data.openOrders.length > 0 &&
stopTarget !== null && !isNaN(stopTarget) && // Ensure stopTarget is valid
gb.data.bid < stopTarget
);

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Order handling

if (entryConditions) {
console.log('Keltner Crossover: Buy condition met.');
gb.method.buyMarket(buyAmount, gb.data.pairName);
setTimestamp();
} else if (sellConditions) {
console.log('Keltner Crossover: Sell condition met.');
gb.method.sellLimit(gb.data.quoteBalance, sellTarget, gb.data.pairName);
setTimestamp();
} else if (stopConditions) {
console.log('Keltner Crossover: Stop condition met.');
gb.method.sellMarket(gb.data.quoteBalance, gb.data.pairName); // Consider cancelling existing limit sell first
setTimestamp();
}
}

Dual RSI​

// Require external module: 'lodash'.
// Install (e.g., npm i lodash) and copy to 'user_modules'.
const _ = gb.method.require(gb.modulesPath + '/lodash');

// Forced short wait time between runs that perform actions.
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();
enoughTimePassed = true; // Allow action on first run
} else {
if (Date.now() - gb.data.pairLedger.customStratStore.timeCheck > 8000) { // 8 seconds
enoughTimePassed = true;
}
}

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

if (enoughTimePassed) {

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Collect indicator data
if (!gb.data.candlesClose || gb.data.candlesClose.length < 20) { // RSI typically needs more data
console.log('Dual RSI: Not enough candle data.');
return;
}

let rsi10Values;
let rsi20Values;

gb.method.tulind.indicators.rsi.indicator([gb.data.candlesClose], [10], function (err, results) {
if (err) { console.error('Error calculating RSI10 for Dual RSI:', err); return; }
rsi10Values = results[0];
});

gb.method.tulind.indicators.rsi.indicator([gb.data.candlesClose], [20], function (err, results) {
if (err) { console.error('Error calculating RSI20 for Dual RSI:', err); return; }
rsi20Values = results[0];
});

// Wait for async indicators to be calculated.
if (typeof rsi10Values === 'undefined' || typeof rsi20Values === 'undefined') {
console.log('Dual RSI indicators not ready yet.');
return;
}
// Ensure there are at least 2 values for crossover check.
if (rsi10Values.length < 2 || rsi20Values.length < 2) {
console.log('Dual RSI: Insufficient RSI data points for crossover.');
return;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Trading conditions
const rsi10 = rsi10Values[rsi10Values.length - 1];
const rsi10Prev = rsi10Values[rsi10Values.length - 2];
const rsi20 = rsi20Values[rsi20Values.length - 1];
const rsi20Prev = rsi20Values[rsi20Values.length - 2];

const entryConditions = (
!gb.data.gotBag && // Only buy if no bag
rsi10 > rsi20 && // Current RSI10 is above RSI20
rsi10Prev < rsi20Prev // Previous RSI10 was below RSI20 (crossover)
);

const exitConditions = (
gb.data.gotBag &&
rsi10 < rsi20 && // Current RSI10 is below RSI20
rsi10Prev > rsi20Prev // Previous RSI10 was above RSI20 (crossunder)
);

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Order handling
const tradingLimit = parseFloat(gb.data.pairLedger.whatstrat.TRADING_LIMIT);
if (isNaN(tradingLimit) || tradingLimit <=0) {
console.log('TRADING_LIMIT not valid for Dual RSI strategy.');
return;
}
const buyAmount = tradingLimit / gb.data.bid;

if (entryConditions) {
console.log('Dual RSI: Buy condition met.');
gb.method.buyMarket(buyAmount, gb.data.pairName);
setTimestamp();
} else if (exitConditions) {
console.log('Dual RSI: Sell condition met.');
gb.method.sellMarket(gb.data.quoteBalance, gb.data.pairName);
setTimestamp();
}
}

Trail Price After StochRSI Target​

// Require external module: 'lodash'.
// Install (e.g., npm i lodash) and copy to 'user_modules'.
const _ = gb.method.require(gb.modulesPath + '/lodash');

// Forced short wait time between runs that perform actions.
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();
enoughTimePassed = true; // Allow action on first run
} else {
if (Date.now() - gb.data.pairLedger.customStratStore.timeCheck > 8000) { // 8 seconds
enoughTimePassed = true;
}
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Functions used in strategy

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

const clearTrailingTargets = function () {
if (!gb.data.pairLedger.customStratStore) gb.data.pairLedger.customStratStore = {};
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) {
// Ensure customStratStore exists before trying to access its properties
if (_.isNil(gb.data.pairLedger.customStratStore)) {
gb.data.pairLedger.customStratStore = {};
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Start price trailing after hitting StochRSI target
const tradingLimit = parseFloat(gb.data.pairLedger.whatstrat.TRADING_LIMIT);
if (isNaN(tradingLimit) || tradingLimit <=0) {
console.log('TRADING_LIMIT not valid for StochRSI trailing strategy.');
return;
}
const buyAmount = tradingLimit / gb.data.bid;

// Handle buy trailing: start trailing after StochRSI target hits (e.g., < 0.1 for oversold)
if (_.isNil(gb.data.pairLedger.customStratStore.buyTrailingTarget) && gb.data.stochRsi < 0.1 && !gb.data.gotBag) {
// Set initial trailing stop (e.g., 0.5% above current ask price)
gb.data.pairLedger.customStratStore.buyTrailingTarget = gb.data.ask * 1.005;
console.log(`StochRSI Trail: Initial buy trailing target set at ${gb.data.pairLedger.customStratStore.buyTrailingTarget}`);
} else if (!_.isNil(gb.data.pairLedger.customStratStore.buyTrailingTarget) && gb.data.pairLedger.customStratStore.buyTrailingTarget > gb.data.ask * 1.005) {
// Update trailing stop if price moves favorably (ask price drops, so new target is lower)
gb.data.pairLedger.customStratStore.buyTrailingTarget = gb.data.ask * 1.005;
console.log(`StochRSI Trail: Buy trailing target updated to ${gb.data.pairLedger.customStratStore.buyTrailingTarget}`);
}

// Handle sell trailing: start trailing after StochRSI target hits (e.g., > 0.9 for overbought)
if (_.isNil(gb.data.pairLedger.customStratStore.sellTrailingTarget) && gb.data.stochRsi > 0.9 && gb.data.gotBag) {
// Set initial trailing stop (e.g., 0.5% below current bid price)
gb.data.pairLedger.customStratStore.sellTrailingTarget = gb.data.bid * 0.995;
console.log(`StochRSI Trail: Initial sell trailing target set at ${gb.data.pairLedger.customStratStore.sellTrailingTarget}`);
} else if (!_.isNil(gb.data.pairLedger.customStratStore.sellTrailingTarget) && gb.data.pairLedger.customStratStore.sellTrailingTarget < gb.data.bid * 0.995) {
// Update trailing stop if price moves favorably (bid price rises, so new target is higher)
gb.data.pairLedger.customStratStore.sellTrailingTarget = gb.data.bid * 0.995;
console.log(`StochRSI Trail: Sell trailing target updated to ${gb.data.pairLedger.customStratStore.sellTrailingTarget}`);
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Fire orders when hitting a trailing stop; remove trailing stop after placing order

// Check if buy trailing target exists and current ask price has crossed above it
if (!_.isNil(gb.data.pairLedger.customStratStore.buyTrailingTarget) && gb.data.ask > gb.data.pairLedger.customStratStore.buyTrailingTarget) {
console.log(`StochRSI Trail: Buy order triggered at ask ${gb.data.ask} (target ${gb.data.pairLedger.customStratStore.buyTrailingTarget})`);
gb.method.buyMarket(buyAmount, gb.data.pairName);
clearTrailingTargets(); // Clear both buy and sell trailing targets
setTimestamp();
}
// Check if sell trailing target exists and current bid price has crossed below it
else if (!_.isNil(gb.data.pairLedger.customStratStore.sellTrailingTarget) && gb.data.bid < gb.data.pairLedger.customStratStore.sellTrailingTarget) {
console.log(`StochRSI Trail: Sell order triggered at bid ${gb.data.bid} (target ${gb.data.pairLedger.customStratStore.sellTrailingTarget})`);
gb.method.sellMarket(gb.data.quoteBalance, gb.data.pairName);
clearTrailingTargets(); // Clear both buy and sell trailing targets
setTimestamp();
}
}