Files
Superalgos/Trading-Process/Interval.js
T
2018-03-05 03:21:33 +01:00

1644 lines
65 KiB
JavaScript

exports.newInterval = function newInterval(BOT, UTILITIES, AZURE_FILE_STORAGE, DEBUG_MODULE, MARKETS_MODULE, POLONIEX_CLIENT_MODULE) {
let bot = BOT;
const GMT_SECONDS = ':00.000 GMT+0000';
const GMT_MILI_SECONDS = '.000 GMT+0000';
const ONE_DAY_IN_MILISECONDS = 24 * 60 * 60 * 1000;
const MODULE_NAME = "Interval";
const LOG_INFO = true;
const EXCHANGE_NAME = "Poloniex";
const EXCHANGE_ID = 1;
const TRADES_FOLDER_NAME = "Trades";
const CANDLES_FOLDER_NAME = "Candles";
const CANDLE_STAIRS_FOLDER_NAME = "Candle-Stairs";
const VOLUMES_FOLDER_NAME = "Volumes";
const VOLUME_STAIRS_FOLDER_NAME = "Volume-Stairs";
const GO_RANDOM = false;
const FORCE_MARKET = 2; // This allows to debug the execution of an specific market. Not intended for production. *
const logger = DEBUG_MODULE.newDebugLog();
logger.fileName = MODULE_NAME;
logger.bot = bot;
interval = {
initialize: initialize,
start: start
};
let fs = require('fs');
let markets;
let charlyAzureFileStorage = AZURE_FILE_STORAGE.newAzureFileStorage(bot);
let oliviaAzureFileStorage = AZURE_FILE_STORAGE.newAzureFileStorage(bot);
let tomAzureFileStorage = AZURE_FILE_STORAGE.newAzureFileStorage(bot);
let mariamAzureFileStorage = AZURE_FILE_STORAGE.newAzureFileStorage(bot);
let utilities = UTILITIES.newUtilities(bot);
return interval;
function initialize(yearAssigend, monthAssigned, callBackFunction) {
try {
/* IMPORTANT NOTE:
We are ignoring in this Interval the received Year and Month. This interval is not depending on Year Month since it procecess the whole market at once.
*/
logger.fileName = MODULE_NAME;
const logText = "[INFO] initialize - Entering function 'initialize' ";
console.log(logText);
logger.write(logText);
charlyAzureFileStorage.initialize("Charly");
oliviaAzureFileStorage.initialize("Olivia");
tomAzureFileStorage.initialize("Tom");
mariamAzureFileStorage.initialize("Mariam");
markets = MARKETS_MODULE.newMarkets(bot);
markets.initialize(callBackFunction);
} catch (err) {
const logText = "[ERROR] initialize - ' ERROR : " + err.message;
console.log(logText);
logger.write(logText);
}
}
/*
This process is going to do the following:
Read the candles and volumes from Olivia and produce for each market two files with candles stairs and volumes stairs respectively.
*/
function start(callBackFunction) {
try {
if (LOG_INFO === true) {
logger.write("[INFO] Entering function 'start'");
}
/*
This bot will trade with a pseudo strategy based on candle and volumes stairs patterns. Essentially it will look at the patterns
it is in at different time periods and try to make a guess if it is a good time to buy, sell, put or remove positions.
The bot trades only at one market: USDT_BTC.
*/
let nextIntervalExecution = false; // This tell weather the Interval module will be executed again or not. By default it will not unless some hole have been found in the current execution.
let nextIntervalLapse = 10 * 1000; // If something fails and we need to retry after a few seconds, we will use this amount of time to request a new execution.
let marketFilesPeriods =
'[' +
'[' + 24 * 60 * 60 * 1000 + ',' + '"24-hs"' + ']' + ',' +
'[' + 12 * 60 * 60 * 1000 + ',' + '"12-hs"' + ']' + ',' +
'[' + 8 * 60 * 60 * 1000 + ',' + '"08-hs"' + ']' + ',' +
'[' + 6 * 60 * 60 * 1000 + ',' + '"06-hs"' + ']' + ',' +
'[' + 4 * 60 * 60 * 1000 + ',' + '"04-hs"' + ']' + ',' +
'[' + 3 * 60 * 60 * 1000 + ',' + '"03-hs"' + ']' + ',' +
'[' + 2 * 60 * 60 * 1000 + ',' + '"02-hs"' + ']' + ',' +
'[' + 1 * 60 * 60 * 1000 + ',' + '"01-hs"' + ']' + ']';
marketFilesPeriods = JSON.parse(marketFilesPeriods);
let dailyFilePeriods =
'[' +
'[' + 45 * 60 * 1000 + ',' + '"45-min"' + ']' + ',' +
'[' + 40 * 60 * 1000 + ',' + '"40-min"' + ']' + ',' +
'[' + 30 * 60 * 1000 + ',' + '"30-min"' + ']' + ',' +
'[' + 20 * 60 * 1000 + ',' + '"20-min"' + ']' + ',' +
'[' + 15 * 60 * 1000 + ',' + '"15-min"' + ']' + ',' +
'[' + 10 * 60 * 1000 + ',' + '"10-min"' + ']' + ',' +
'[' + 05 * 60 * 1000 + ',' + '"05-min"' + ']' + ',' +
'[' + 04 * 60 * 1000 + ',' + '"04-min"' + ']' + ',' +
'[' + 03 * 60 * 1000 + ',' + '"03-min"' + ']' + ',' +
'[' + 02 * 60 * 1000 + ',' + '"02-min"' + ']' + ',' +
'[' + 01 * 60 * 1000 + ',' + '"01-min"' + ']' + ']';
dailyFilePeriods = JSON.parse(dailyFilePeriods);
const market = {
assetA: "USDT",
assetB: "BTC",
};
/*
Here we will keep the last status report, to be available during the whole process. The Status Report is a file the bot process
reads and saves again after each execution. Its main purpose is to know when the last execution was in order to locate the execution
context. When the bot runs for the first time it takes some vital parameters from there and it checks them through its lifecycle to see
if they changed. The Status Report file can eventually be manipulated by the bot operators / developers in order to change those parameters
or to point the last execution to a different date. Humans are not supose to manipulate the Execution Histroy or the execution Context files.
The Execution History is basically an index with dates of all the executions the bot did across its history. It allows the bot plotter
to know which datetimes have informacion about the bots execution in order to display it.
The Execution Context file records all the context information of the bot at the moment of execution and the final state of all of its
positions on the market.
*/
let statusReport; // This is the typical Status Report that defines which was the last sucessfull execution and a few other pieces of info.
let executionHistory; // This is
let executionContext; // Here is the information of the last execution of this bot process.
let processDatetime = new Date(); // This will be considered the process date and time, so as to have it consistenly all over the execution.
processDatetime = new Date(processDatetime.valueOf() - 30 * 24 * 60 * 60 * 1000); // we go 30 days back in time since candles are currently not up to date.
/*
During the process we will create a new History Record. This will go to the Context History file which essentially mantains an
index of all the bots executions. This file will later be plotted by the bot s plotter on the timeline, allowing end users to
know where there is information related to the actions taken by the bot.
*/
let newHistoryRecord = {
date: processDatetime,
rate: 0, // This will be used to know where to plot this information in the time line.
newPositions: 0,
newTrades: 0,
movedPositions: 0
};
let exchangePositions = []; // These are the open positions at the exchange at the account the bot is authorized to use.
let openPositions = []; // These are the open positions the bot knows it made by itself.
let candlesMap = new Map; // This bot will look only at the last 10 candles for each Time Period and they will be stored here.
let stairsMap = new Map; // This bot will use this pattern. Here we will store the pattern we are in for each Time Period.
let apiKey = readApiKey();
let poloniexApiClient = new POLONIEX_CLIENT_MODULE(apiKey.Key, apiKey.Secret);
getPositionsAtExchange();
function readApiKey() {
try {
return JSON.parse(fs.readFileSync('../' + 'API-Keys' + '/' + EXCHANGE_NAME + '.json', 'utf8'));
}
catch (err) {
const logText = "[ERROR] 'readApiKey' - ERROR : " + err.message;
logger.write(logText);
}
}
function getPositionsAtExchange() {
/*
Here we grab all the positions at the exchange for the account we are using for trading. We will not asume all the positions
were made by this bot, but we need them all to later check if all the ones made by the bot are still there, were executed or
manually cancelled by the account owner.
*/
poloniexApiClient.returnOpenOrders(market.assetA, market.assetB, onExchangeCallReturned);
function onExchangeCallReturned(err, exchangeResponse) {
try {
if (err || exchangeResponse.error !== undefined) {
try {
if (err.message.indexOf("ETIMEDOUT") > 0) {
const logText = "[WARN] onExchangeCallReturned - Timeout reached while trying to access the Exchange API. Requesting new execution later. : ERROR = " + err.message;
logger.write(logText);
/* We abort the process and request a new execution at the configured amount of time. */
callBackFunction(true, nextIntervalLapse);
return;
} else {
if (err.message.indexOf("ECONNRESET") > 0) {
const logText = "[WARN] onExchangeCallReturned - The exchange reseted the connection. Requesting new execution later. : ERROR = " + err.message;
logger.write(logText);
/* We abort the process and request a new execution at the configured amount of time. */
callBackFunction(true, nextIntervalLapse);
return;
} else {
const logText = "[ERROR] onExchangeCallReturned - Unexpected error trying to contact the Exchange. This will halt this bot process. : ERROR = " + err.message;
logger.write(logText);
return;
}
}
} catch (err) {
const logText = "[ERROR] onExchangeCallReturned : ERROR : exchangeResponse.error = " + exchangeResponse.error;
logger.write(logText);
return;
}
return;
} else {
/*
This is what we receive from the exchange. We will convert this to our standard format for later use.
[ { orderNumber: '151918418632',
type: 'sell',
rate: '20000.00000000',
startingAmount: '0.00010000',
amount: '0.00010000',
total: '2.00000000',
date: '2018-02-24 11:14:17',
margin: 0 } ]
*/
for (let i = 0; i < exchangeResponse.length; i++) {
let openPosition = {
id: exchangeResponse[i].orderNumber,
type: exchangeResponse[i].type,
rate: exchangeResponse[i].rate,
amountA: exchangeResponse[i].total,
amountB: exchangeResponse[i].amount,
date: (new Date(exchangeResponse[i].date)).valueOf()
};
exchangePositions.push(openPosition);
}
readStatusAndContext();
}
}
catch (err) {
const logText = "[ERROR] 'onExchangeCallReturned' - ERROR : " + err.message;
logger.write(logText);
}
}
}
function readStatusAndContext() {
/*
Here we get the positions the bot did and that are recorded at the bot storage account. We will use them through out the rest
of the process.
*/
getStatusReport();
function getStatusReport() {
/* If the process run and was interrupted, there should be a status report that allows us to resume execution. */
let fileName = "Status.Report.json"
let filePath = EXCHANGE_NAME + "/" + bot.name + "/" + bot.dataSetVersion + "/Processes/" + bot.process;
mariamAzureFileStorage.getTextFile(filePath, fileName, onFileReceived, true);
function onFileReceived(text) {
try {
statusReport = JSON.parse(text);
if (statusReport.lastExecution === undefined) {
createConext();
} else {
getExecutionHistory();
}
} catch (err) {
/*
It might happen that the file content is corrupt or it does not exist. The bot can not run without a Status Report,
since it is risky to ignore its own history, so even for first time execution, a status report with the right format
is needed.
*/
const logText = "[ERROR] 'getStatusReport' - Bot cannot execute without a status report. ERROR : " + err.message;
logger.write(logText);
}
}
}
function getExecutionHistory() {
let fileName = "Execution.History.json"
let filePath = EXCHANGE_NAME + "/" + bot.name + "/" + bot.dataSetVersion + "/Output/" + bot.process;
mariamAzureFileStorage.getTextFile(filePath, fileName, onFileReceived, true);
function onFileReceived(text) {
try {
executionHistory = JSON.parse(text);
getExecutionContext();
} catch (err) {
/*
It might happen that the file content is corrupt or it does not exist. The bot can not run without a Status Report,
since it is risky to ignore its own history, so even for first time execution, a status report with the right format
is needed.
*/
const logText = "[ERROR] 'getExecutionHistory' - Bot cannot execute without the Execution History. ERROR : " + err.message;
logger.write(logText);
}
}
}
function getExecutionContext() {
let date = new Date(statusReport.lastExecution);
let fileName = "Execution.Context.json"
let dateForPath = date.getUTCFullYear() + '/' + utilities.pad(date.getUTCMonth() + 1, 2) + '/' + utilities.pad(date.getUTCDate(), 2) + '/' + utilities.pad(date.getUTCHours(), 2) + '/' + utilities.pad(date.getUTCMinutes(), 2);
let filePath = EXCHANGE_NAME + "/" + bot.name + "/" + bot.dataSetVersion + "/Output/" + bot.process + "/" + dateForPath;
mariamAzureFileStorage.getTextFile(filePath, fileName, onFileReceived, true);
function onFileReceived(text) {
try {
executionContext = JSON.parse(text);
executionContext.transactions = []; // We record here the transactions that happened duting this execution.
ordersExecutionCheck();
} catch (err) {
/*
It might happen that the file content is corrupt or it does not exist. The bot can not run without a Status Report,
since it is risky to ignore its own history, so even for first time execution, a status report with the right format
is needed.
*/
const logText = "[ERROR] 'getExecutionContext' - Bot cannot execute without the Execution Context. ERROR : " + err.message;
logger.write(logText);
}
}
}
}
function createConext() {
/*
When the bot is executed for the very first time, there are a few files that do not exist and need to be created, and that
is what we are going to do now.
*/
executionHistory = [];
executionContext = {
investment: {
assetA: 0,
assetB: 0
},
availableBalance: {
assetA: 0,
assetB: 0
},
positions: [],
transactions: []
};
getCandles();
}
function ordersExecutionCheck() {
/*
Here we check that all the positions we know we have are still at the exchange. If they are not, we will try to take appropiate
actions. Reasons why the positions might not be there are:
1. The user / account owner closed the positions manually.
2. The exchange for some eventuality closed the positions. In some exchanges positions have an expiratin time.
3. The orders were executed.
For as 1 and 2 are similar unexpected situations and we will stop the bot execution when we detect we are there. Number 3 is an
expected behaviour and we take appropiate action.
Also, the position might still be there but it might have been partially executed. To detect that we need to compare the
position amounts we have on file to the one we are receiving from the exchange.
Before we begin, we have to remove all the orders that have been already closed at the last execution of the bot.
*/
let openPositions = [];
for (let a = 0; a < executionContext.positions.length; a++) {
if (executionContext.positions[a].status === "open") {
openPositions.push(executionContext.positions[a]);
}
}
executionContext.positions = openPositions;
/* Now we can start checking what happened at the exchange. */
let i = 0;
controlLoop();
function loopBody() {
for (let j = 0; j < exchangePositions.length; j++) {
if (executionContext.positions[i].id === exchangePositions[j].id) {
/*
We need to know if the order was partially executed. To know that, we compare the amounts on file with the ones
received from the exchange. If they are the same, then the order is intact. Otherwise, to confirm a partial execution,
we will request the associated trades from the exchange.
*/
if (executionContext.positions[i].amountB === parseFloat(exchangePositions[j].amountB)) {
/* Position is still there, untouched. Nothing to do here. */
next();
return;
} else {
getOrderTradesAtExchange(executionContext.positions[i].id, confirmOrderWasPartiallyExecuted);
return;
function confirmOrderWasPartiallyExecuted(trades) {
/*
To confirm everything is ok, we will add all the amounts on trades plus the remaining amounts
at the order at the exchange and they must be equal to the one on file. Otherwise something very strange could
have happened, in which case we will halt the bot execution.
*/
let sumAssetA = 0;
let sumAssetB = 0;
for (k = 0; k < trades.length; k++) {
sumAssetA = sumAssetA + trades[k].amountA;
sumAssetB = sumAssetB + trades[k].amountB;
}
/* To this we add the current position amounts. */
sumAssetA = sumAssetA + exchangePositions[j].amountA;
sumAssetB = sumAssetB + exchangePositions[j].amountB;
/* And finally we add the fees */
sumAssetA = sumAssetA + exchangePositions[j].fee;
if (
executionContext.positions[i].amountA !== sumAssetA ||
executionContext.positions[i].amountB !== sumAssetB
) {
const logText = "[ERROR] 'confirmOrderWasPartiallyExecuted' - Cannot be confirmed that a partially execution was done well. Bot stopping execution. ";
logger.write(logText);
return;
}
/*
Confirmed that order was partially executed. Next thing to do is to remember the trades and the new position.
*/
executionContext.positions[i].amountA = exchangePositions[j].amountA;
executionContext.positions[i].amountB = exchangePositions[j].amountB;
executionContext.positions[i].date = (new Date(exchangePositions[j].date)).valueOf();
for (k = 0; k < trades.length; k++) {
executionContext.positions[i].trades.push(trades[k]);
newHistoryRecord.newTrades++;
}
let newTransaction = {
type: executionContext.positions[i].type + " partially executed",
position: executionContext.positions[i]
};
executionContext.transactions.push(newTransaction);
/* All done. */
next();
}
}
}
}
/* Position not found: we need to know if the order was executed. */
getOrderTradesAtExchange(executionContext.positions[i].id, confirmOrderWasExecuted);
function confirmOrderWasExecuted(trades) {
/*
To confirm everything is ok, we will add all the amounts on trades asociated to the order and
they must be equal to the one on file. Otherwise something very strange could have happened,
in which case we will halt the bot execution.
*/
let sumAssetA = 0;
let sumAssetB = 0;
for (k = 0; k < trades.length; k++) {
sumAssetA = sumAssetA + trades[k].amountA;
sumAssetB = sumAssetB + trades[k].amountB;
}
/* We add the fees */
sumAssetA = sumAssetA + exchangePositions[j].fee;
if (
executionContext.positions[i].amountA !== sumAssetA ||
executionContext.positions[i].amountB !== sumAssetB
) {
const logText = "[ERROR] 'confirmOrderWasExecuted' - Cannot be confirmed that the order was executed. It must be manually cancelled by the user or cancelled by the exchange itself. Bot stopping execution. ";
logger.write(logText);
return;
}
/*
Confirmed that order was executed. Next thing to do is to remember the trades and change its status.
*/
executionContext.positions[i].status = "executed";
for (k = 0; k < trades.length; k++) {
executionContext.positions[i].trades.push(trades[k]);
newHistoryRecord.newTrades++;
}
let newTransaction = {
type: executionContext.positions[i].type + " executed",
position: executionContext.positions[i]
};
executionContext.transactions.push(newTransaction);
/* All done. */
next();
}
}
function next() {
i++;
controlLoop();
}
function controlLoop() {
if (i < executionContext.positions.length) {
loopBody();
} else {
final();
}
}
function final() {
getCandles();
}
}
function getOrderTradesAtExchange(orderId, callBackFunction) {
/*
Given one order, we request all the associated trades to it.
*/
poloniexApiClient.returnOrderTrades(orderId, onExchangeCallReturned);
function onExchangeCallReturned(err, exchangeResponse) {
try {
if (err || exchangeResponse.error !== undefined) {
try {
if (err.message.indexOf("ETIMEDOUT") > 0) {
const logText = "[WARN] getOrderTradesAtExchange - onExchangeCallReturned - Timeout reached while trying to access the Exchange API. Requesting new execution later. : ERROR = " + err.message;
logger.write(logText);
/* We abort the process and request a new execution at the configured amount of time. */
callBackFunction(true, nextIntervalLapse);
return;
} else {
if (err.message.indexOf("ECONNRESET") > 0) {
const logText = "[WARN] getOrderTradesAtExchange - onExchangeCallReturned - The exchange reseted the connection. Requesting new execution later. : ERROR = " + err.message;
logger.write(logText);
/* We abort the process and request a new execution at the configured amount of time. */
callBackFunction(true, nextIntervalLapse);
return;
} else {
const logText = "[ERROR] getOrderTradesAtExchange - onExchangeCallReturned - Unexpected error trying to contact the Exchange. This will halt this bot process. : ERROR = " + err.message;
logger.write(logText);
return;
}
}
} catch (err) {
const logText = "[ERROR] getOrderTradesAtExchange -onExchangeCallReturned : ERROR : exchangeResponse.error = " + exchangeResponse.error;
logger.write(logText);
return;
}
return;
} else {
/*
This is what we receive from the exchange. We will convert this to our standard format for later use.
[
{
"globalTradeID": 20825863,
"tradeID": 147142,
"currencyPair":
"BTC_XVC",
"type": "buy",
"rate": "0.00018500",
"amount": "455.34206390",
"total": "0.08423828",
"fee": "0.00200000",
"date": "2016-03-14 01:04:36"
},
...]
*/
let trades = [];
for (let i = 0; i < exchangeResponse.length; i++) {
let trade = {
id: exchangeResponse[i].tradeID,
type: exchangeResponse[i].type,
rate: exchangeResponse[i].rate,
amountA: exchangeResponse[i].total,
amountB: exchangeResponse[i].amount,
fee: exchangeResponse[i].fee,
date: (new Date(exchangeResponse[i].date)).valueOf()
}
trades.push(trade);
}
callBackFunction(trades);
}
}
catch (err) {
const logText = "[ERROR] 'getOrderTradesAtExchange - onExchangeCallReturned' - ERROR : " + err.message;
logger.write(logText);
}
}
}
function getCandles() {
/*
We will read several files with candles for the current day. We will use these files as an input
to make trading decitions later.
*/
let candlesFiles = new Map;
getMarketFiles();
function getMarketFiles() {
/* Now we will get the market files */
for (i = 0; i < marketFilesPeriods.length; i++) {
let periodTime = marketFilesPeriods[i][0];
let periodName = marketFilesPeriods[i][1];
getFile(oliviaAzureFileStorage, "@AssetA_@AssetB.json", "@Exchange/Output/Candles/Multi-Period-Market/@Period", periodName, undefined, onFileReceived);
function onFileReceived(file) {
candlesFiles.set(periodName, file);
if (candlesFiles.size === marketFilesPeriods.length) {
getDailyFiles();
}
}
}
}
function getDailyFiles() {
for (i = 0; i < dailyFilePeriods.length; i++) {
let periodTime = dailyFilePeriods[i][0];
let periodName = dailyFilePeriods[i][1];
getFile(oliviaAzureFileStorage, "@AssetA_@AssetB.json", "@Exchange/Output/Candles/Multi-Period-Daily/@Period/@Year/@Month/@Day", periodName, processDatetime, onFileReceived);
function onFileReceived(file) {
candlesFiles.set(periodName, file);
if (candlesFiles.size === dailyFilePeriods.length + marketFilesPeriods.length) {
getCandlesWeAreIn();
}
}
}
}
function getCandlesWeAreIn() {
let counter = 0;
candlesFiles.forEach(getCurrentCandles);
function getCurrentCandles(pCandlesFile, pPeriodName, map) {
let candlesArray = [];
for (i = 0; i < pCandlesFile.length; i++) {
let candle = {
open: undefined,
close: undefined,
min: 10000000000000,
max: 0,
begin: undefined,
end: undefined,
direction: undefined
};
candle.min = pCandlesFile[i][0];
candle.max = pCandlesFile[i][1];
candle.open = pCandlesFile[i][2];
candle.close = pCandlesFile[i][3];
candle.begin = pCandlesFile[i][4];
candle.end = pCandlesFile[i][5];
if (candle.open > candle.close) { candle.direction = 'down'; }
if (candle.open < candle.close) { candle.direction = 'up'; }
if (candle.open === candle.close) { candle.direction = 'side'; }
/* We are going to store the last 10 candles per period, which will give the bot a good sense of where it is. */
let timePeriod = candle.end - candle.begin + 1; // In miliseconds. (remember each candle spans a period minus one milisecond)
if (candle.begin >= processDatetime.valueOf() - timePeriod * 10 && candle.end <= processDatetime.valueOf()) {
candlesArray.push(candle);
}
}
candlesMap.set(pPeriodName, candlesArray);
counter++;
if (counter === candlesFiles.size) {
getPatterns();
}
}
}
}
function getPatterns() {
/*
We will read several files with pattern calculations for the current day. We will use these files as an input
to make trading decitions later.
*/
let stairsFiles = new Map;
getMarketFiles();
function getMarketFiles() {
/* Now we will get the market files */
for (i = 0; i < marketFilesPeriods.length; i++) {
let periodTime = marketFilesPeriods[i][0];
let periodName = marketFilesPeriods[i][1];
getFile(tomAzureFileStorage, "@AssetA_@AssetB.json", "@Exchange/Tom/dataSet.V1/Output/Candle-Stairs/Multi-Period-Market/@Period", periodName, undefined, onFileReceived);
function onFileReceived(file) {
stairsFiles.set(periodName, file);
if (stairsFiles.size === marketFilesPeriods.length) {
getDailyFiles();
}
}
}
}
function getDailyFiles() {
for (i = 0; i < dailyFilePeriods.length; i++) {
let periodTime = dailyFilePeriods[i][0];
let periodName = dailyFilePeriods[i][1];
getFile(tomAzureFileStorage, "@AssetA_@AssetB.json", "@Exchange/Tom/dataSet.V1/Output/Candle-Stairs/Multi-Period-Daily/@Period/@Year/@Month/@Day", periodName, processDatetime, onFileReceived);
function onFileReceived(file) {
stairsFiles.set(periodName, file);
if (stairsFiles.size === dailyFilePeriods.length + marketFilesPeriods.length) {
getStairsWeAreIn();
}
}
}
}
function getStairsWeAreIn() {
let counter = 0;
stairsFiles.forEach(getCurrentStairs);
function getCurrentStairs(pStairsFile, pPeriodName, map) {
for (i = 0; i < pStairsFile.length; i++) {
let stairs = {
open: undefined,
close: undefined,
min: 10000000000000,
max: 0,
begin: undefined,
end: undefined,
direction: undefined,
candleCount: 0,
firstMin: 0,
firstMax: 0,
lastMin: 0,
lastMax: 0
};
stairs.open = pStairsFile[i][0];
stairs.close = pStairsFile[i][1];
stairs.min = pStairsFile[i][2];
stairs.max = pStairsFile[i][3];
stairs.begin = pStairsFile[i][4];
stairs.end = pStairsFile[i][5];
stairs.direction = pStairsFile[i][6];
stairs.candleCount = pStairsFile[i][7];
stairs.firstMin = pStairsFile[i][8];
stairs.firstMax = pStairsFile[i][9];
stairs.lastMin = pStairsFile[i][10];
stairs.lastMax = pStairsFile[i][11];
if (processDatetime.valueOf() >= stairs.begin && processDatetime.valueOf() <= stairs.end) {
stairsMap.set(pPeriodName, stairs);
}
}
counter++;
if (counter === stairsFiles.size) {
businessLogic();
}
}
}
}
function getFile(pFileService, pFileName, pFilePath, pPeriodName, pDatetime, callBackFunction) {
pFileName = pFileName.replace("@AssetA", market.assetA);
pFileName = pFileName.replace("@AssetB", market.assetB);
pFilePath = pFilePath.replace("@Exchange", EXCHANGE_NAME);
pFilePath = pFilePath.replace("@Period", pPeriodName);
if (pDatetime !== undefined) {
pFilePath = pFilePath.replace("@Year", pDatetime.getUTCFullYear());
pFilePath = pFilePath.replace("@Month", utilities.pad(pDatetime.getUTCMonth() + 1, 2));
pFilePath = pFilePath.replace("@Day", utilities.pad(pDatetime.getUTCDate(), 2));
}
pFileService.getTextFile(pFilePath, pFileName, onFileReceived);
function onFileReceived(text) {
let data;
try {
data = JSON.parse(text);
} catch (err) {
data = JSON.parse("[]");
}
callBackFunction(data);
}
}
function businessLogic() {
/*
This bot will trade at the 10 min Time Period, receive an investment in BTC and will try to produce profits in BTC as well.
So we will produce the best posible strategy using only stairs pattern so as to have a working example. To simplify this
first bot, we will also use all the available balance at once. That means that we will have at most one open position at the same
time.
First thing we need to know is to see where we are:
Do we have open positions?
If not, shall we create one?
If yes, shall we move them?
*/
if (executionContext.positions.length > 0) {
if (executionContext.positions[0].type === "buy") {
validateBuyPosition(executionContext.positions[0]);
} else {
validateSellPosition(executionContext.positions[0]);
}
} else {
/*
This bot is expected to always have an open position, either buy or sell. If it does not have one, that means that it is
running for the first time. In which case, we will create one sell position at a very high price. Later, once the bot executes
again, it will take it and move it to a reasonable place and monitor it during each execution round.
Lets see first which is the current price.
*/
let candleArray = candlesMap.get("01-min");
let candle = candleArray[candleArray.length - 1]; // The last candle of the 10 candles array for the 1 min Time Period.
let currentRate = candle.close;
/* Now we verify that this candle is not too old. Lets say no more than 5 minutes old. */
if (candle.begin < processDatetime.valueOf() - 5 * 60 * 1000) {
const logText = "[WARN] businessLogic - Last one min candle more than 5 minutes old. Bot cannot operate with this delay. Retrying later." ;
logger.write(logText);
/* We abort the process and request a new execution at the configured amount of time. */
callBackFunction(true, nextIntervalLapse);
return;
}
/*
As we just want to create the first order now and we do not want this order to get executed, we will put it at
the +50% of current exchange rate. Next Bot execution will move it strategically.
*/
let rate = candle.close * 1.50;
/*
The Status Report contains the main parameters for the bot, which are:
Which is the amount of each Asset the bot is authorized to start with. If the bots gets profits, it might use the profits
to trade as well.
Which asset is the one which profits should be accumulated. That is the base Asset, where the bot is standing while not
trading. In the case of this simple bot, its base asset will be BTC.
*/
let AmountA = statusReport.initialBalance.amountA;
let AmountB = statusReport.initialBalance.amountB;
/* We are going to sell all AmountB */
AmountA = AmountB * rate;
putPositionAtExchange("sell", rate, AmountA, AmountB, writeStatusAndContext);
}
function validateBuyPosition(pPosition) {
/* For simplicity of this example bot, we will do the same than when we are selling. */
validateSellPosition(pPosition);
}
function validateSellPosition(pPosition) {
let candleArray;
let candle;
let weight;
/*
Keeping in mind this is an example of traing bot, we are going to put some logic here that in the end will move the current position
up or down. It will move it down if the bot feels it is time to sell, and up if it feels that selling is not a good idea.
To achieve a final rate to move the current position at the exchange, we are going to go through the available candles and patterns
and each one is going to make a micro-move, and at the end we will have a final rate to send a move command to the exchange.
We will use a weight to give more or less importance to different Time Periods.
*/
let diff;
let variationPercentage;
let timePeriodName;
let targetRate = pPosition.rate;
let weightArray = [1 / (24 * 60), 1 / (12 * 60), 1 / (8 * 60), 1 / (6 * 60), 1 / (4 * 60), 1 / (3 * 60), 1 / (2 * 60), 1 / (1 * 60)];
for (i = 0; i < marketFilesPeriods.length; i++) {
weight = weightArray[i];
timePeriodName = marketFilesPeriods[i][1];
candleArray = candlesMap.get(timePeriodName);
candle = candleArray[candleArray.length - 1]; // The last candle of the 10 candles array.
diff = candle.close - candle.open;
variationPercentage = diff * 100 / candle.open; // This is the % of how much the rate increased or decreced from open to close.
targetRate = targetRate + targetRate * variationPercentage / 100 * weight;
}
/* Finally we move the order position to where we have just estimated is a better place. */
movePositionAtExchange(pPosition, targetRate, writeStatusAndContext);
}
}
function putPositionAtExchange(pType, pRate, pAmountA, pAmountB, callBackFunction) {
if (pType === "buy") {
poloniexApiClient.buy(market.assetA, market.assetB, pRate, pAmountB, onExchangeCallReturned);
} else {
poloniexApiClient.sell(market.assetA, market.assetB, pRate, pAmountB, onExchangeCallReturned);
}
function onExchangeCallReturned(err, exchangeResponse) {
try {
if (err || exchangeResponse.error !== undefined) {
try {
if (err.message.indexOf("ETIMEDOUT") > 0) {
const logText = "[WARN] putPositionAtExchange - onExchangeCallReturned - Timeout reached while trying to access the Exchange API. Requesting new execution later. : ERROR = " + err.message;
logger.write(logText);
/* We abort the process and request a new execution at the configured amount of time. */
callBackFunction(true, nextIntervalLapse);
return;
} else {
if (err.message.indexOf("ECONNRESET") > 0) {
const logText = "[WARN] putPositionAtExchange - onExchangeCallReturned - The exchange reseted the connection. Requesting new execution later. : ERROR = " + err.message;
logger.write(logText);
/* We abort the process and request a new execution at the configured amount of time. */
callBackFunction(true, nextIntervalLapse);
return;
} else {
const logText = "[ERROR] putPositionAtExchange -onExchangeCallReturned - Unexpected error trying to contact the Exchange. This will halt this bot process. : ERROR = " + err.message;
logger.write(logText);
return;
}
}
} catch (err) {
const logText = "[ERROR] putPositionAtExchange - onExchangeCallReturned : ERROR : exchangeResponse.error = " + exchangeResponse.error;
logger.write(logText);
return;
}
return;
} else {
/*
This is what we can receive from the exchange. We will ignore the trades list for now, and analize
what happened at the next bot execution.
{
"orderNumber":31226040,
"resultingTrades":
[{
"amount":"338.8732",
"date":"2014-10-18 23:03:21",
"rate":"0.00000173",
"total":"0.00058625",
"tradeID":"16164",
"type":"buy"
}]
}
*/
/*
We will add this new position to the executionContext.
*/
let position = {
id: exchangeResponse.orderNumber,
type: pType,
rate: pRate,
amountA: pAmountA,
amountB: pAmountB,
date: (processDatetime.valueOf()),
status: "open",
trades: []
};
executionContext.positions.push(position);
let newTransaction = {
type: "newPosition",
position: position
};
executionContext.transactions.push(newTransaction);
newHistoryRecord.newPositions++;
newHistoryRecord.rate = pRate;
callBackFunction();
}
}
catch (err) {
const logText = "[ERROR] 'putPositionAtExchange - onExchangeCallReturned' - ERROR : " + err.message;
logger.write(logText);
}
}
}
function movePositionAtExchange(pPosition, pNewRate, callBackFunction) {
poloniexApiClient.moveOrder(pPosition.id, pNewRate, pPosition.amountB, onExchangeCallReturned);
function onExchangeCallReturned(err, exchangeResponse) {
try {
if (err || exchangeResponse.error !== undefined) {
try {
if (err.message.indexOf("ETIMEDOUT") > 0 || err.message.indexOf("Order execution timed out") > 0) {
const logText = "[WARN] movePositionAtExchange - onExchangeCallReturned - Timeout reached while trying to access the Exchange API. Requesting new execution later. : ERROR = " + err.message;
logger.write(logText);
/* We abort the process and request a new execution at the configured amount of time. */
callBackFunction(true, nextIntervalLapse);
return;
} else {
if (err.message.indexOf("ECONNRESET") > 0) {
const logText = "[WARN] movePositionAtExchange - onExchangeCallReturned - The exchange reseted the connection. Requesting new execution later. : ERROR = " + err.message;
logger.write(logText);
/* We abort the process and request a new execution at the configured amount of time. */
callBackFunction(true, nextIntervalLapse);
return;
} else {
const logText = "[ERROR] movePositionAtExchange -onExchangeCallReturned - Unexpected error trying to contact the Exchange. This will halt this bot process. : ERROR = " + err.message;
logger.write(logText);
return;
}
}
} catch (err) {
const logText = "[ERROR] movePositionAtExchange - onExchangeCallReturned : ERROR : exchangeResponse.error = " + exchangeResponse.error;
logger.write(logText);
return;
}
return;
} else {
/*
This is what we can receive from the exchange. We will ignore the trades list for now, and analize
what happened at the next bot execution.
{
"orderNumber":31226040,
"resultingTrades":
[{
"amount":"338.8732",
"date":"2014-10-18 23:03:21",
"rate":"0.00000173",
"total":"0.00058625",
"tradeID":"16164",
"type":"buy"
}]
}
*/
/*
We will add this new position to the executionContext.
*/
if (exchangeResponse.success !== 1) {
const logText = "[ERROR] movePositionAtExchange -onExchangeCallReturned - Exchange said operation was not successful. Since we do not know what this means we will halt this bot process. : ERROR = " + err.message;
logger.write(logText);
return;
}
let newPosition = {
id: exchangeResponse.orderNumber,
type: pPosition.type,
rate: pNewRate,
amountA: pPosition.amountB * pNewRate,
amountB: pPosition.amountB,
date: (processDatetime.valueOf()),
status: "open",
trades: []
};
let oldPosition = JSON.parse(JSON.stringify(pPosition));
/* We need to update the position we have on file. */
for (let i = 0; i < executionContext.positions.length; i++) {
if (executionContext.positions[i].id === pPosition.id) {
executionContext.positions[i] = newPosition;
break;
}
}
let newTransaction = {
type: "movePosition",
oldPosition: oldPosition,
newPosition: newPosition
};
executionContext.transactions.push(newTransaction);
newHistoryRecord.movedPositions++;
newHistoryRecord.rate = pNewRate;
callBackFunction();
}
}
catch (err) {
const logText = "[ERROR] 'movePositionAtExchange -onExchangeCallReturned' - ERROR : " + err.message;
logger.write(logText);
}
}
}
function writeStatusAndContext() {
writeExecutionContext();
function writeExecutionContext() {
if (LOG_INFO === true) {
logger.write("[INFO] Entering function 'writeExecutionContext'");
}
try {
let fileName = "Execution.Context.json"
let dateForPath = processDatetime.getUTCFullYear() + '/' + utilities.pad(processDatetime.getUTCMonth() + 1, 2) + '/' + utilities.pad(processDatetime.getUTCDate(), 2) + '/' + utilities.pad(processDatetime.getUTCHours(), 2) + '/' + utilities.pad(processDatetime.getUTCMinutes(), 2);
let filePath = EXCHANGE_NAME + "/" + bot.name + "/" + bot.dataSetVersion + "/Output/" + bot.process + "/" + dateForPath;
utilities.createFolderIfNeeded(filePath, mariamAzureFileStorage, onFolderCreated);
function onFolderCreated() {
try {
let fileContent = JSON.stringify(executionContext);
mariamAzureFileStorage.createTextFile(filePath, fileName, fileContent + '\n', onFileCreated);
function onFileCreated() {
if (LOG_INFO === true) {
logger.write("[INFO] 'writeExecutionContext' - Content written: " + fileContent);
}
writeExucutionHistory();
}
}
catch (err) {
const logText = "[ERROR] 'writeExecutionContext - onFolderCreated' - ERROR : " + err.message;
logger.write(logText);
}
}
}
catch (err) {
const logText = "[ERROR] 'writeExecutionContext' - ERROR : " + err.message;
logger.write(logText);
}
}
function writeExucutionHistory() {
if (LOG_INFO === true) {
logger.write("[INFO] Entering function 'writeExucutionHistory'");
}
try {
let fileName = "Execution.History.json"
let filePath = EXCHANGE_NAME + "/" + bot.name + "/" + bot.dataSetVersion + "/Output/" + bot.process;
utilities.createFolderIfNeeded(filePath, mariamAzureFileStorage, onFolderCreated);
function onFolderCreated() {
try {
let newRecord = [
newHistoryRecord.date.valueOf(),
newHistoryRecord.rate,
newHistoryRecord.newPositions,
newHistoryRecord.newTrades,
newHistoryRecord.movedPositions
];
executionHistory.push(newRecord);
let fileContent = JSON.stringify(executionHistory);
mariamAzureFileStorage.createTextFile(filePath, fileName, fileContent + '\n', onFileCreated);
function onFileCreated() {
if (LOG_INFO === true) {
logger.write("[INFO] 'writeExucutionHistory'");
}
writeStatusReport();
}
}
catch (err) {
const logText = "[ERROR] 'writeExucutionHistory - onFolderCreated' - ERROR : " + err.message;
logger.write(logText);
}
}
}
catch (err) {
const logText = "[ERROR] 'writeExucutionHistory' - ERROR : " + err.message;
logger.write(logText);
}
}
function writeStatusReport() {
if (LOG_INFO === true) {
logger.write("[INFO] Entering function 'writeStatusReport'");
}
try {
let fileName = "Status.Report.json"
let filePath = EXCHANGE_NAME + "/" + bot.name + "/" + bot.dataSetVersion + "/Processes/" + bot.process;
utilities.createFolderIfNeeded(filePath, mariamAzureFileStorage, onFolderCreated);
function onFolderCreated() {
try {
statusReport.lastExecution = processDatetime;
let fileContent = JSON.stringify(statusReport);
mariamAzureFileStorage.createTextFile(filePath, fileName, fileContent + '\n', onFileCreated);
function onFileCreated() {
if (LOG_INFO === true) {
logger.write("[INFO] 'writeStatusReport' - Content written: " + fileContent);
}
callBackFunction(true); // We tell the AA Platform that we request a regular execution and finish the bot s process.
return;
}
}
catch (err) {
const logText = "[ERROR] 'writeStatusReport - onFolderCreated' - ERROR : " + err.message;
logger.write(logText);
}
}
}
catch (err) {
const logText = "[ERROR] 'writeStatusReport' - ERROR : " + err.message;
logger.write(logText);
}
}
}
}
catch (err) {
const logText = "[ERROR] 'Start' - ERROR : " + err.message;
logger.write(logText);
}
}
};