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

522 lines
24 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 show logs on the browser console on 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:
dataDependencies: Retrieve all dependencies to other bots defined on the configuration
putPosition: Put a buy or sell position at the exchange (only limit at this point)
movePosition: Move an existing position
getPositions: Obtain all the positions existing on the exchange
getBalance: Obtain the total balance on the exchange known by the platform (at this point 0.001 BTC)
getAvailableBalance: Obtain the current available balance on the exchange known by the platform
getInvestment: Obtain initial investment known by the platform (at this point 0.001 BTC)
getProfits: Get current profits at this point in time
getCombinedProfits: Get current profits
getROI: Get current ROI
getMarketRate: Get current market rate
getTicker: Gets the current highest bid and lowest ask on the exchaneg
sendMessage: Put a visual message on the platform (it could be at different zoom levels 1-10 )
rememberThis: Store a string variable accross executions by key value pairs
remindMeOf: Get an stored string value by key
sendEmail: Send an email during execution as notifications
*/
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 to 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 Algobot, to form an 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
In this case, this particular bot will use Gauss indicator bot, other
indicator bots are already available. You can use any indicator you want,
even you can create a new one if you need.
*/
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.
You can find more details fo how this work here:
https://advancedalgos.net/documentation-interface.shtml
*/
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.
Here is a detailed explanation of the bot:
https://github.com/AAVikings/AAArtudito-Trading-Bot/blob/master/README.md
*/
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) {
try {
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)
}
} catch (err) {
logger.write(MODULE_NAME, '[ERROR] start -> businessLogic -> createBuyPosition -> err = ' + err.message)
callBackFunction(global.DEFAULT_FAIL_RESPONSE)
}
}
function createSellPosition (callBack) {
try{
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)
}
} catch (err) {
logger.write(MODULE_NAME, '[ERROR] start -> businessLogic -> createBuyPosition -> err = ' + err.message)
callBackFunction(global.DEFAULT_FAIL_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()
}
}
}
}
}