Files
Superalgos/Trading-Process/User.Bot.js
T

487 lines
21 KiB
JavaScript

exports.newUserBot = function newUserBot (bot, logger) {
/*
Creating a new instance from the platform of this bot:
- bot: An instance of the bot configuration
- logger: An instance of the platform logger that allow both: to save logs
on cloud execution and browser execution
*/
// Variable used for logs, this will be passed to the logger instance
const MODULE_NAME = 'User Bot'
// Debug log level
const FULL_LOG = true
// Info log level
const LOG_INFO = true
/*
The reference to the Trading Platform Advanced Algos Assistant that will
allow to put positions on the exchange.
*/
let assistant
/*
This is a dependency example to other bots, in this case to the the
LRC Indicator.
*/
let gaussStorage
/*
This objects returns two public functions that will be used to integrate
with the platform.
*/
let thisObject = {
initialize: initialize,
start: start
}
return thisObject
/*
The initialize function must be implemented by all trading bots.
pAssistant: the instance of the platform which methods will allow tu put getPositions
pGenes: This variable will be used during initialization to get the parameters
from the platform. At the moment it won't be used but in future releases
it will allow to create different clones of the algonet.
*/
function initialize (pAssistant, pGenes, callBackFunction) {
try {
if (LOG_INFO === true) { logger.write(MODULE_NAME, '[INFO] initialize -> Entering function.') }
// Integration with platform logger, when running on cloud
logger.fileName = MODULE_NAME
/*
We keep a local reference to the assistant that will allow to put
positions on the exchange and it's created and managed by the platform
*/
assistant = pAssistant
/*
On 'assistant.dataDependencies.dataSets' we will receive the data files
retrieved for those defined on the bot configuration
Note that the key is:
TeamName-BotName-BotProductFolder-BotProcessName-BotDataSetVersion
*/
let key = 'AAVikings-AAGauss-LRC-Points-Multi-Period-Daily-dataSet.V1'
gaussStorage = assistant.dataDependencies.dataSets.get(key)
/*
Once Completed we must return the global.DEFAULT_OK_RESPONSE
*/
callBackFunction(global.DEFAULT_OK_RESPONSE)
} catch (err) {
logger.write(MODULE_NAME, '[ERROR] initialize -> onDone -> err = ' + err.message)
callBackFunction(global.DEFAULT_FAIL_RESPONSE)
}
}
/*
The start function is called by the platform for executing the bot every 1 minute
*/
function start (callBackFunction) {
if (LOG_INFO === true) { logger.write(MODULE_NAME, '[INFO] start -> Entering function.') }
/*
This breakpoint will be called once the web platform reach the graphical stop point
*/
if (global.AT_BREAKPOINT === true) {
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] run -> loop -> Plot Breakpoint Hit."); }
}
const market = global.MARKET
const CHANNEL_DOWN = -1
const CHANNEL_UP = 1
const NO_CHANNEL = 0
/*
Here we call the main function that will check buy, sell or move conditions.
*/
businessLogic(onDone)
/*
This function will be called after we complete all validations and operations,
we make sure everything was ok before returning the control to the platform.
The platform will check this 3 situations:
global.DEFAULT_OK_RESPONSE: Proceed with normal execution
global.DEFAULT_RETRY_RESPONSE: Retry after 10 seconds
global.DEFAULT_FAIL_RESPONSE: Finish in failure state
(this allow us to check the logs and fix execution errors when they occur)
*/
function onDone (err) {
try {
switch (err.result) {
case global.DEFAULT_OK_RESPONSE.result: {
logger.write(MODULE_NAME, '[INFO] start -> onDone -> Execution finished well. :-)')
callBackFunction(global.DEFAULT_OK_RESPONSE)
return
}
case global.DEFAULT_RETRY_RESPONSE.result: { // Something bad happened, but if we retry in a while it might go through the next time.
logger.write(MODULE_NAME, '[ERROR] start -> onDone -> Retry Later. Requesting Execution Retry.')
callBackFunction(global.DEFAULT_RETRY_RESPONSE)
return
}
case global.DEFAULT_FAIL_RESPONSE.result: { // This is an unexpected exception that we do not know how to handle.
logger.write(MODULE_NAME, '[ERROR] start -> onDone -> Operation Failed. Aborting the process. err = ' + err.message)
callBackFunction(global.DEFAULT_FAIL_RESPONSE)
return
}
}
} catch (err) {
logger.write(MODULE_NAME, '[ERROR] start -> onDone -> err = ' + err.message)
callBackFunction(global.DEFAULT_FAIL_RESPONSE)
}
}
/*
We will check the direction of the channel and based on that create a buy or sell
position on the market.
*/
function businessLogic (callBack) {
if (LOG_INFO === true) { logger.write(MODULE_NAME, '[INFO] start -> businessLogic -> Entering function.') }
// Simulation for forcing buy or sell on each execution without checking the rules
let forceBuyAndSell = false
if(forceBuyAndSell){
if (Math.random(1) <= 0.5){
createBuyPosition(callBack);
}else{
createSellPosition(callBack);
}
}else{
// Here we really check the bot business logic
getChannelTilt(firstTiltCheck);
}
function firstTiltCheck (err, channelTilt) {
try {
if (LOG_INFO === true) { logger.write(MODULE_NAME, '[INFO] start -> businessLogic -> firstTiltCheck -> Entering Function.') }
if (err.result !== global.DEFAULT_OK_RESPONSE.result) {
logger.write(MODULE_NAME, '[ERROR] start -> businessLogic -> firstTiltCheck -> err = ' + err.message)
callBack(global.DEFAULT_FAIL_RESPONSE)
} else {
if (channelTilt == 1) {
createBuyPosition(callBack)
} else if (channelTilt == -1) {
createSellPosition(callBack)
} else {
if (LOG_INFO === true) { logger.write(MODULE_NAME, "[INFO] start -> businessLogic -> firstTiltCheck -> Nothing to do, there isn't a buy or sell opportunity.") }
callBack(global.DEFAULT_OK_RESPONSE)
}
}
} catch (err) {
logger.write(MODULE_NAME, '[ERROR] start -> businessLogic -> firstTiltCheck -> err = ' + err.message)
callBackFunction(global.DEFAULT_FAIL_RESPONSE)
}
}
/*
Here we will get the file from the indicator bot, that have already all values calculated.
This will provide more efficiency and allow other bots to consume the services as well.
*/
function getChannelTilt (callBack) {
if (LOG_INFO === true) { logger.write(MODULE_NAME, '[INFO] start -> businessLogic -> getChannelTilt -> Entering function.') }
let lrcPoint, previousLRCPoint
let queryDate = new Date(bot.processDatetime)
getLRCPointsFile(queryDate, onLRCPointsFileReceived)
function onLRCPointsFileReceived (err, lrcPointsFile) {
try {
if (LOG_INFO === true) { logger.write(MODULE_NAME, '[INFO] start -> businessLogic -> getChannelTilt -> onLRCPointsFileReceived.') }
let lastIndexLrcPointsFile = lrcPointsFile.length - 1
let lastAvailableDateTime = lrcPointsFile[lastIndexLrcPointsFile][0]
if (bot.processDatetime.valueOf() <= lastAvailableDateTime || !isExecutionToday()) {
for (let i = 0; i < lrcPointsFile.length; i++) {
if (bot.processDatetime.valueOf() >= lrcPointsFile[i][0] && bot.processDatetime.valueOf() < lrcPointsFile[i][1]) {
lrcPoint = {
begin: lrcPointsFile[i][0],
end: lrcPointsFile[i][1],
min: lrcPointsFile[i][2],
mid: lrcPointsFile[i][3],
max: lrcPointsFile[i][4]
}
if (i >= 1) {
let previous = i - 1
previousLRCPoint = {
begin: lrcPointsFile[previous][0],
end: lrcPointsFile[previous][1],
min: lrcPointsFile[previous][2],
mid: lrcPointsFile[previous][3],
max: lrcPointsFile[previous][4]
}
applyBotRules(lrcPoint, previousLRCPoint, callBack)
} else {
queryDate.setDate(queryDate.getDate() - 1)
getLRCPointsFile(queryDate, onPreviousLRCPointsFileReceived)
}
return
}
}
logger.write(MODULE_NAME, '[ERROR] start -> businessLogic -> getChannelTilt -> onLRCPointsFileReceived. The expected LRC Point was not found: ' + bot.processDatetime.valueOf())
callBack(global.DEFAULT_FAIL_RESPONSE)
} else {
// Running live we will process last available LRCPoint only if it's delayed 25 minutes top
let maxTolerance = 25 * 60 * 1000
if (bot.processDatetime.valueOf() <= (lastAvailableDateTime + maxTolerance)) {
lrcPoint = {
begin: lrcPointsFile[lastIndexLrcPointsFile][0],
end: lrcPointsFile[lastIndexLrcPointsFile][1],
min: lrcPointsFile[lastIndexLrcPointsFile][2],
mid: lrcPointsFile[lastIndexLrcPointsFile][3],
max: lrcPointsFile[lastIndexLrcPointsFile][4]
}
if (lastIndexLrcPointsFile >= 1) {
let previous = lastIndexLrcPointsFile - 1
previousLRCPoint = {
begin: lrcPointsFile[previous][0],
end: lrcPointsFile[previous][1],
min: lrcPointsFile[previous][2],
mid: lrcPointsFile[previous][3],
max: lrcPointsFile[previous][4]
}
applyBotRules(lrcPoint, previousLRCPoint, callBack)
} else {
queryDate.setDate(queryDate.getDate() - 1)
getLRCPointsFile(queryDate, onPreviousLRCPointsFileReceived)
}
} else {
if (LOG_INFO === true) logger.write(MODULE_NAME, '[WARN] start -> getChannelTilt -> onLRCPointsFileReceived. Available candle older than 5 minutes. Skipping execution.')
callBack(global.DEFAULT_OK_RESPONSE, NO_CHANNEL)
}
}
} catch (err) {
logger.write(MODULE_NAME, '[ERROR] start -> getChannelTilt -> onLRCPointsFileReceived -> err = ' + err.message)
callBackFunction(global.DEFAULT_FAIL_RESPONSE)
}
}
/*
Depending on the time requested, the necessary information could be on the
file from the day before.
*/
function onPreviousLRCPointsFileReceived (err, lrcPointsFile) {
if (LOG_INFO === true) { logger.write(MODULE_NAME, '[INFO] start -> getChannelTilt -> onLRCPointsFileReceived.') }
let previous = lrcPointsFile.length - 1
previousLRCPoint = {
begin: lrcPointsFile[previous][0],
end: lrcPointsFile[previous][1],
min: lrcPointsFile[previous][2],
mid: lrcPointsFile[previous][3],
max: lrcPointsFile[previous][4]
}
applyBotRules(lrcPoint, previousLRCPoint, callBack)
}
}
/*
We have all the information needed to proceed applying all the rules.
*/
function applyBotRules (lrcPoint, previousLRCPoint, callBack) {
if (LOG_INFO === true) { logger.write(MODULE_NAME, '[INFO] start -> applyBotRules -> Entering Function.') }
let channelTilt = NO_CHANNEL
let ruleApplied = ''
if (lrcPoint.max < lrcPoint.mid && lrcPoint.min > lrcPoint.mid && lrcPoint.mid < lrcPoint.min) {
if (lrcPoint.min > previousLRCPoint.min && lrcPoint.mid > previousLRCPoint.mid && lrcPoint.max > previousLRCPoint.max) {
// The channel points UP
channelTilt = CHANNEL_UP
ruleApplied += 'Rule_1.'
}
}
if (lrcPoint.min < lrcPoint.mid && lrcPoint.max > lrcPoint.mid && lrcPoint.mid < lrcPoint.max) {
if (lrcPoint.min < previousLRCPoint.min && lrcPoint.mid < previousLRCPoint.mid && lrcPoint.max < previousLRCPoint.max) {
// The channel points DOWN
channelTilt = CHANNEL_DOWN
ruleApplied += 'Rule_2.'
}
}
if (lrcPoint.min < previousLRCPoint.min && lrcPoint.mid <= previousLRCPoint.mid) {
// 15 AND 30 changed direction from up to down
channelTilt = CHANNEL_DOWN
ruleApplied += 'Rule_3a.'
}
if (lrcPoint.min > previousLRCPoint.min && lrcPoint.mid >= previousLRCPoint.mid) {
// 15 AND 30 changed direction from down to up
channelTilt = CHANNEL_UP
ruleApplied += 'Rule_3b.'
}
let logMessage = bot.processDatetime.toISOString() + '. Rule: ' + ruleApplied + '. Tilt: ' + channelTilt + ' (' + Number(lrcPoint.min).toLocaleString() + '|' + Number(lrcPoint.mid).toLocaleString() + '|' + Number(lrcPoint.max).toLocaleString() + ')'
if (LOG_INFO === true) { logger.write(MODULE_NAME, '[INFO] start -> getChannelTilt -> applyBotRules -> Results: ' + logMessage) }
callBack(global.DEFAULT_OK_RESPONSE, channelTilt)
}
/*
Here we will get the file from the indicator bot, that have already all values calculated.
This will provide more efficiency and allow other bots to consume the services as well.
*/
function getLRCPointsFile (dateTime, callback) {
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] start -> businessLogic -> getLRCPointsFile -> Entering function.') }
let datePath = dateTime.getUTCFullYear() + '/' + pad(dateTime.getUTCMonth() + 1, 2) + '/' + pad(dateTime.getUTCDate(), 2)
let filePath = 'LRC-Points/Multi-Period-Daily/30-min/' + datePath
let fileName = market.assetA + '_' + market.assetB + '.json'
/*
bot.botCache: allow us to keep a map (key value pairs) between executions,
so we don't need to go to the storage to retrieve this value.
If the value already exist on the cache we will get it from there,
otherwise it will be retrieved from the bot storage.
*/
if (bot.startMode === 'Backtest' && bot.botCache.has(filePath)) {
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] start -> businessLogic -> getLRCPointsFile -> Getting the file from local cache.') }
cleanUpCache(bot.botCache, 'LRC-Points/Multi-Period-Daily/30-min', dateTime)
callback(global.DEFAULT_OK_RESPONSE, bot.botCache.get(filePath))
} else {
/*
The structure of the indicator files and the file itself retrieved
is explained on the indicator bot readme file.
*/
gaussStorage.getTextFile(filePath, fileName, onFileReceived)
}
function onFileReceived (err, text) {
if (err.result === global.DEFAULT_OK_RESPONSE.result) {
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] start -> getChannelTilt -> getLRCPointsFile -> onFileReceived > Entering Function.') }
let lrcPointsFile = JSON.parse(text)
bot.botCache.set(filePath, lrcPointsFile)
callback(global.DEFAULT_OK_RESPONSE, lrcPointsFile)
} else {
logger.write(MODULE_NAME, '[ERROR] start -> getChannelTilt -> getLRCPointsFile -> onFileReceived -> Failed to get the file. Will abort the process and request a retry.')
callBackFunction(global.DEFAULT_RETRY_RESPONSE)
return
}
}
function cleanUpCache (fileCache, path, dateTimeCacheFile) {
let date = new Date(dateTimeCacheFile)
date.setDate(date.getDate() - 2)
let datePath = date.getUTCFullYear() + '/' + pad(date.getUTCMonth() + 1, 2) + '/' + pad(date.getUTCDate(), 2)
let cachePosition = path + '/' + datePath
// Remove and go back one day
if (fileCache.has(cachePosition)) {
fileCache.delete(cachePosition)
date.setDate(date.getDate() - 1)
cleanUpCache(fileCache, date)
}
}
}
function createBuyPosition (callBack) {
if (LOG_INFO === true) { logger.write(MODULE_NAME, '[INFO] start -> businessLogic -> createBuyPosition -> Entering function.') }
// We get the current positions we have on the exchange
let positions = assistant.getPositions()
let assetABalance = assistant.getAvailableBalance().assetA
let currentRate = assistant.getTicker().ask // *.9
let amountA = assistant.getAvailableBalance().assetA
let amountB = Number((amountA / currentRate).toFixed(8))
if (positions.length > 0 && positions[0].type === 'buy' && positions[0].status !== 'executed') {
if (LOG_INFO === true) { logger.write(MODULE_NAME, '[INFO] start -> businessLogic -> createBuyPosition -> Moving an existing BUY position to a new price: $' + Number(currentRate).toLocaleString()) }
let message = 'Moving an existing buy position to a new price: $' + Number(currentRate).toLocaleString()
message += '. Combined ROI on current execution: ' + getCombinedProfit() + '%. '
assistant.sendMessage(6, 'Moving Position', message)
message = bot.processDatetime.toISOString() + ' - ' + message
assistant.sendEmail('Alerts', message, bot.emailSubscriptions)
assistant.movePosition(positions[0], currentRate, callBack)
} else if (assetABalance > 0) {
if (LOG_INFO === true) { logger.write(MODULE_NAME, '[INFO] start -> businessLogic -> createBuyPosition -> Artuditu put a new BUY position at price: $' + Number(currentRate).toLocaleString()) }
let message = 'Creating a new buy position. Price: $' + Number(currentRate).toLocaleString()
message += '. Combined ROI on current execution: ' + getCombinedProfit() + '%. '
assistant.sendMessage(7, 'Buying', message)
message = bot.processDatetime.toISOString() + ' - ' + message
assistant.sendEmail('Alerts', message, bot.emailSubscriptions)
assistant.putPosition('buy', currentRate, amountA, amountB, callBack)
} else {
if (LOG_INFO === true) { logger.write(MODULE_NAME, '[INFO] start -> businessLogic -> createBuyPosition -> Not enough available balance to buy.') }
callBack(global.DEFAULT_OK_RESPONSE)
}
}
function createSellPosition (callBack) {
if (LOG_INFO === true) { logger.write(MODULE_NAME, '[INFO] start -> businessLogic -> createSellPosition -> Entering function.') }
// We get the current positions we have on the exchange
let positions = assistant.getPositions()
let assetBBalance = assistant.getAvailableBalance().assetB
let currentRate = assistant.getTicker().bid // *1.1
let amountB = assistant.getAvailableBalance().assetB
let amountA = amountB * currentRate
if (positions.length > 0 && positions[0].type === 'sell' && positions[0].status !== 'executed') {
if (LOG_INFO === true) { logger.write(MODULE_NAME, '[INFO] start -> businessLogic -> createSellPosition -> Artuditu is moving an existing SELL position to a new price: $' + Number(currentRate).toLocaleString()) }
let message = 'Moving an existing sell position to a new price: $' + Number(currentRate).toLocaleString()
message += '. Combined ROI on current execution: ' + getCombinedProfit() + '%. '
assistant.sendMessage(6, 'Moving Position', message)
message = bot.processDatetime.toISOString() + ' - ' + message
assistant.sendEmail('Alerts', message, bot.emailSubscriptions)
assistant.movePosition(positions[0], currentRate, callBack)
} else if (assetBBalance > 0) {
if (LOG_INFO === true) { logger.write(MODULE_NAME, '[INFO] start -> businessLogic -> createSellPosition -> Artuditu put a new SELL position at price: $' + Number(currentRate).toLocaleString()) }
let message = 'Creating a new sell position. Price: $' + Number(currentRate).toLocaleString()
message += '. Combined ROI on current execution: ' + getCombinedProfit() + '%. '
assistant.sendMessage(7, 'Selling', message)
message = bot.processDatetime.toISOString() + ' - ' + message
assistant.sendEmail('Alerts', message, bot.emailSubscriptions)
assistant.putPosition('sell', currentRate, amountA, amountB, callBack)
} else {
if (LOG_INFO === true) { logger.write(MODULE_NAME, '[INFO] start -> businessLogic -> createSellPosition -> There is not enough available balance to sell.') }
callBack(global.DEFAULT_OK_RESPONSE)
}
}
function pad (str, max) {
str = str.toString()
return str.length < max ? pad('0' + str, max) : str
}
function isExecutionToday () {
let localDate = new Date()
let today = new Date(Date.UTC(
localDate.getUTCFullYear(),
localDate.getUTCMonth(),
localDate.getUTCDate()))
return (today.getUTCFullYear() === bot.processDatetime.getUTCFullYear()
&& today.getUTCMonth() === bot.processDatetime.getUTCMonth()
&& today.getUTCDate() === bot.processDatetime.getUTCDate())
}
function getCombinedProfit () {
if (assistant.getCombinedProfits() !== undefined) {
if (assistant.getCombinedProfits().assetA > 0) {
return Number(assistant.getCombinedProfits().assetA).toLocaleString()
} else {
return Number(assistant.getCombinedProfits().assetB).toLocaleString()
}
}
}
}
}