diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 000000000..dfe077042
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+# Auto detect text files and perform LF normalization
+* text=auto
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..82c487bdd
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,11 @@
+
+.vs/*
+obj/*
+
+bin/*
+
+*.config
+*.log
+*.cache
+node_modules/*
+
diff --git a/.project b/.project
new file mode 100644
index 000000000..6ff263000
--- /dev/null
+++ b/.project
@@ -0,0 +1,11 @@
+
+
+ AAArtudito-Trading-Bot
+
+
+
+
+
+
+
+
diff --git a/AAArtudito-Trading-Bot.sln b/AAArtudito-Trading-Bot.sln
new file mode 100644
index 000000000..0e3d52f62
--- /dev/null
+++ b/AAArtudito-Trading-Bot.sln
@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+VisualStudioVersion = 15.0.27130.2027
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}") = "AAArtudito", "AAArtudito.njsproj", "{A54D4656-3DB8-407A-9333-E65115E994AB}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {A54D4656-3DB8-407A-9333-E65115E994AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A54D4656-3DB8-407A-9333-E65115E994AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A54D4656-3DB8-407A-9333-E65115E994AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A54D4656-3DB8-407A-9333-E65115E994AB}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {B4BDAEAF-E863-458A-87C1-CC6D8C490784}
+ EndGlobalSection
+EndGlobal
diff --git a/AAArtudito.njsproj b/AAArtudito.njsproj
new file mode 100644
index 000000000..c845e44e4
--- /dev/null
+++ b/AAArtudito.njsproj
@@ -0,0 +1,89 @@
+
+
+
+ Debug
+ 2.0
+ {a54d4656-3db8-407a-9333-e65115e994ab}
+
+ ShowAllFiles
+ User.Bot.js
+
+
+ .
+ {3AF33F2E-1136-4D97-BBB7-1795711AC8B8};{349c5851-65df-11da-9384-00065b846f21};{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}
+ 14.0
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+ Project
+ Debug|Any CPU
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ False
+ True
+ 0
+ /
+ http://localhost:48022/
+ False
+ True
+ http://localhost:1337
+ False
+
+
+
+
+
+
+ CurrentPage
+ True
+ False
+ False
+ False
+
+
+
+
+
+
+
+
+ False
+ False
+
+
+
+
+
\ No newline at end of file
diff --git a/Artudito.png b/Artudito.png
new file mode 100644
index 000000000..1a3a0aae5
Binary files /dev/null and b/Artudito.png differ
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 000000000..10e295746
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,24 @@
+Financial Beings License
+
+Collective Copyright by the Advanced Algos Community
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software including without limitation the rights to use, copy, modify,
+merge, publish, distribute and sublicense copies of the Software, and to permit
+persons to whom the Software is furnished to do so, subject to the following
+conditions:
+
+ 1. Using, copying, modifying, publishing, distributing and sublicensing rights
+ are restricted to the Advanced Algos Platform, meaning the rights granted can
+ only be exercised through the Advanced Algos Service.
+
+ 2. The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 000000000..66be6dd69
--- /dev/null
+++ b/README.md
@@ -0,0 +1,52 @@
+# I'm Artudito, a t-bot!
+AKA a trading algobot running on the AAPlatform by Advanced Algos Ltd.
+
+### My Specialty
+I work on the USDT-BTC market with a strategy based on linear regression curves.
+
+### Ideal for
+Trading short-lived but established trends.
+
+### Details
+
+| **Name** | **Type** | **Version** | **Release Date** | **Current dataSet** |
+|----------|----------|----------|----------|----------|
+| Artudito |Trading | 1.0 | May 2018 | dataSet.V1 |
+
+# My Strategy
+
+### Markets
+* **Exchanges**: Poloniex
+* **Markets**: USDT-BTC
+
+### Description
+
+I use the analysis performed by [AAMasters Gauss i-bot](https://github.com/AAMasters/AAGauss-Indicator-Bot) on recent historic prices in which he applies a [linear regression]( https://en.wikipedia.org/wiki/Linear_regression) to a set of price values in the 15, 30 and 60 minutes candles to obtain model linear regression curves. Based on those curves, I apply my own ruleset.
+
+#### Basic Rule Set
+
+* If the tilt of the linear regression curve points up on the 15, 30, and 60 minutes scale then I go long.
+* If the tilt of the linear regression curve points down on the 15, 30, and 60 minutes scale then I go short.
+* If the linear regression curve of the 15 and 30 minutes scale change direction, I exit.
+
+#### Examples
+* The asset is trending up on the 15, 30 and 60 minutes scale. Then the 15 minutes curve turns down and the 30 minutes curve goes flat: I exit.
+* The asset is trending down on all three indicators. Then the 15 minutes curve turns up and the 30 minutes curve turns flat or upwards: I enter.
+* The tilt of the 15, 30 and 60 minutes curves is flat: I don't enter or exit.
+
+### Compatible Plotters
+[AAMasters Plotters-Trading](https://github.com/AAMasters/Plotters-Trading)
+
+### Dependencies
+* [AAMasters Bruce](https://github.com/AAMasters/AABruce-Indicator-Bot)
+* [AAMasters Olivia](https://github.com/AAMasters/AAOlivia-Indicator-Bot)
+* [AAMasters Gauss](https://github.com/AAMasters/AAGauss-Indicator-Bot)
+
+### Trading as a Service (TaaS) Fees
+No fees.
+
+# Disclaimer
+
+THE AA MASTERS BOTS AND THEIR ASSOCIATED PRODUCTS AND SERVICES ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, SUITABILITY FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT.
+
+IN NO EVENT WILL ADVANCED ALGOS BE LIABLE TO ANY PARTY FOR ANY DIRECT, INDIRECT, SPECIAL OR OTHER CONSEQUENTIAL DAMAGES FOR ANY USE OF THE AACLOUD, THE AA ARENA COMPETITION, THE AA MASTERS BOTS, OR ANY OTHER ASSOCIATED SERVICE OR WEB SITE, INCLUDING, WITHOUT LIMITATION, ANY LOST PROFITS, BUSINESS INTERRUPTION, LOSS OF FUNDS, PROGRAMS OR OTHER DATA OR OTHERWISE, EVEN IF WE ARE EXPRESSLY ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
diff --git a/Trading-Process/User.Bot.js b/Trading-Process/User.Bot.js
new file mode 100644
index 000000000..a07b687ea
--- /dev/null
+++ b/Trading-Process/User.Bot.js
@@ -0,0 +1,521 @@
+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 allows both: to save logs
+ on cloud execution and shows logs on the browser console on browser execution
+ */
+
+ // Variable used for logs, which 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 orders 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 exchange
+ sendMessage: Put a visual message on the platform (different zoom levels [1-10] can be handled)
+ rememberThis: Store a string variable across executions by key value pairs
+ remindMeOf: Get a 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 is not 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 allows to put
+ positions on the exchange, 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; other
+ indicators are available. You can use any indicator you want,
+ or create a new one if you should need to.
+ */
+ 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 reaches the graphical stop point.
+ You can find more details on how this works 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."); } // Place breakpoint in this line
+ }
+
+ 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 allows 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 in 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 already has 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 to 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 already has 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: allows 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 retrieved file itself
+ 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 at 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 at 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()
+ }
+ }
+ }
+ }
+}
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 000000000..4c1670f94
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,5 @@
+{
+ "name": "AAArtudito",
+ "version": "1.0.0",
+ "lockfileVersion": 1
+}
diff --git a/package.json b/package.json
new file mode 100644
index 000000000..a3a80b8a2
--- /dev/null
+++ b/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "AAArtudito",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "author": "Matias-Benitez",
+ "license": "ISC",
+ "dependencies": {}
+}
diff --git a/this.bot.config.json b/this.bot.config.json
new file mode 100644
index 000000000..a86fd9f84
--- /dev/null
+++ b/this.bot.config.json
@@ -0,0 +1,209 @@
+{
+ "displayName": "Artudito",
+ "codeName": "AAArtudito",
+ "type": "Trading",
+ "version": {
+ "major": 1,
+ "minor": 0,
+ "patch": 0
+ },
+ "devTeam": "AAVikings",
+ "profilePicture": "Artudito.png",
+ "dataSetVersion": "dataSet.V1",
+ "genes": [
+ {
+ "name": "stopLoss",
+ "description": "This represent the percentage of the stop loss configuration. It could be from 0 (disabled) to 100.",
+ "lowerLimit": 0,
+ "upperLimit": 5
+ }
+ ],
+ "processes": [
+ {
+ "name": "Trading-Process",
+ "description": "Simple trading strategy to be used as a template.",
+ "startMode": {
+ "live": {
+ "run": "true",
+ "resumeExecution": "false"
+ },
+ "backtest": {
+ "run": "false",
+ "beginDatetime": "2018-04-11T00:00:00.000Z",
+ "endDatetime": "2018-06-17T23:59:59.000Z",
+ "waitTime": 1
+ },
+ "competition": {
+ "run": "false",
+ "beginDatetime": "2018-07-04T00:00:00.000Z",
+ "endDatetime": "2018-07-06T23:59:59.000Z",
+ "resumeExecution": "true"
+ }
+ },
+ "normalWaitTime": 10000,
+ "retryWaitTime": 10000,
+ "sleepWaitTime": 3600000,
+ "comaWaitTime": 86400000,
+ "timePeriod": 1800000,
+ "statusDependencies": [
+ {
+ "devTeam": "AAVikings",
+ "bot": "AAGauss",
+ "botVersion": {
+ "major": 1,
+ "minor": 0
+ },
+ "process": "Multi-Period-Daily",
+ "dataSetVersion": "dataSet.V1"
+ },
+ {
+ "devTeam": "AAVikings",
+ "bot": "AAGauss",
+ "botVersion": {
+ "major": 1,
+ "minor": 0
+ },
+ "process": "Multi-Period-Market",
+ "dataSetVersion": "dataSet.V1"
+ },
+ {
+ "devTeam": "AAVikings",
+ "bot": "AAArtudito",
+ "botVersion": {
+ "major": 1,
+ "minor": 0
+ },
+ "process": "Trading-Process",
+ "dataSetVersion": "dataSet.V1"
+ }
+ ],
+ "dataDependencies": [
+ {
+ "devTeam": "AAVikings",
+ "bot": "AAGauss",
+ "botVersion": {
+ "major": 1,
+ "minor": 0
+ },
+ "product": "LRC-Points",
+ "dataSetVersion": "dataSet.V1",
+ "dataSet": "Multi-Period-Daily"
+ },
+ {
+ "devTeam": "AAVikings",
+ "bot": "AAGauss",
+ "botVersion": {
+ "major": 1,
+ "minor": 0
+ },
+ "product": "LRC-Points",
+ "dataSetVersion": "dataSet.V1",
+ "dataSet": "Multi-Period-Market"
+ },
+ {
+ "devTeam": "AAMasters",
+ "bot": "AABruce",
+ "botVersion": {
+ "major": 1,
+ "minor": 0
+ },
+ "product": "Candles",
+ "dataSetVersion": "dataSet.V1",
+ "dataSet": "One-Min"
+ },
+ {
+ "devTeam": "AAMasters",
+ "bot": "AAOlivia",
+ "botVersion": {
+ "major": 1,
+ "minor": 0
+ },
+ "product": "Candles",
+ "dataSet": "Multi-Period-Daily",
+ "dataSetVersion": "dataSet.V1"
+ }
+ ]
+ }
+ ],
+ "products": [
+ {
+ "codeName": "Live Trading History",
+ "displayName": "Live Trading History",
+ "shortDisplayName": "Live",
+ "description": "General information about Artudito trading history.",
+ "storageAccount": "aaartudito",
+ "dataSets": [
+ {
+ "codeName": "Oficial History",
+ "type": "File Sequence",
+ "validPeriods": [ "24-hs", "12-hs", "08-hs", "06-hs", "04-hs", "03-hs", "02-hs", "01-hs", "45-min", "40-min", "30-min", "20-min", "15-min", "10-min", "05-min", "04-min", "03-min", "02-min", "01-min" ],
+ "filePath": "@DevTeam/@Bot.1.0/AACloud.1.1/Poloniex/dataSet.V1/Output/Trading-Process",
+ "fileName": "Execution.History.Live.@Sequence.json"
+ }
+ ],
+ "exchangeList": [
+ {
+ "name": "Poloniex"
+ }
+ ],
+ "plotter": {
+ "devTeam": "AAMasters",
+ "codeName": "PlottersTrading",
+ "moduleName": "History"
+ }
+ },
+ {
+ "codeName": "Backtest Trading History",
+ "displayName": "Backtest Trading History",
+ "shortDisplayName": "Backtest",
+ "description": "General information about Artudito backtest trading history.",
+ "storageAccount": "aaartudito",
+ "dataSets": [
+ {
+ "codeName": "Backtest History",
+ "type": "File Sequence",
+ "validPeriods": [ "24-hs", "12-hs", "08-hs", "06-hs", "04-hs", "03-hs", "02-hs", "01-hs", "45-min", "40-min", "30-min", "20-min", "15-min", "10-min", "05-min", "04-min", "03-min", "02-min", "01-min" ],
+ "filePath": "@DevTeam/@Bot.1.0/AACloud.1.1/Poloniex/dataSet.V1/Output/Trading-Process",
+ "fileName": "Execution.History.Backtest.@Sequence.json"
+ }
+ ],
+ "exchangeList": [
+ {
+ "name": "Poloniex"
+ }
+ ],
+ "plotter": {
+ "devTeam": "AAMasters",
+ "codeName": "PlottersTrading",
+ "moduleName": "History"
+ }
+ },
+ {
+ "codeName": "Competition Trading History",
+ "displayName": "Competition Trading History",
+ "shortDisplayName": "Competition",
+ "description": "General information about Artudito competition trading history.",
+ "storageAccount": "aaartudito",
+ "dataSets": [
+ {
+ "codeName": "Competition History",
+ "type": "File Sequence",
+ "validPeriods": [ "24-hs", "12-hs", "08-hs", "06-hs", "04-hs", "03-hs", "02-hs", "01-hs", "45-min", "40-min", "30-min", "20-min", "15-min", "10-min", "05-min", "04-min", "03-min", "02-min", "01-min" ],
+ "filePath": "@DevTeam/@Bot.1.0/AACloud.1.1/Poloniex/dataSet.V1/Output/Trading-Process",
+ "fileName": "Execution.History.Competition.@Sequence.json"
+ }
+ ],
+ "exchangeList": [
+ {
+ "name": "Poloniex"
+ }
+ ],
+ "plotter": {
+ "devTeam": "AAMasters",
+ "codeName": "PlottersTrading",
+ "moduleName": "History"
+ }
+ }
+ ],
+ "emailSubscriptions": "vikings.alerts@gmail.com"
+}
\ No newline at end of file