Merge Complete
This commit is contained in:
1 parent
482ef7a81a
commit
dabe23d505
6483 files changed
No files matched your search
@@ -0,0 +1,13 @@
|
||||
|
||||
INTER_PROCESS_FILES_PATH=../Inter-Process-Files
|
||||
STORAGE_PATH=../Data-Storage
|
||||
PLOTTERS_PATH=../Legacy-Code
|
||||
|
||||
USE_KEYVAULT=false
|
||||
WRITE_LOGS_TO_FILES=true
|
||||
BOTS_PATH=../Legacy-Code
|
||||
LOG_PATH=../Log-Files
|
||||
|
||||
WEB_SOCKETS_INTERFACE_PORT=18041
|
||||
WEB_SERVER_PORT=34248
|
||||
WEB_SERVER_URL=localhost
|
||||
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch Program",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"program": "${workspaceFolder}\\server.js"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
|
||||
exports.newCloudUtilities = function newCloudUtilities(logger) {
|
||||
|
||||
const FULL_LOG = true;
|
||||
|
||||
const MODULE_NAME = "Cloud Utilities";
|
||||
|
||||
let utilities = {
|
||||
pad: pad,
|
||||
createFolderIfNeeded: createFolderIfNeeded,
|
||||
calculatePresicion: calculatePresicion
|
||||
};
|
||||
|
||||
return utilities;
|
||||
|
||||
function createFolderIfNeeded(path, azureFileStorage, callBackFunction) {
|
||||
|
||||
try {
|
||||
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] createFolderIfNeeded -> Entering function."); }
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] createFolderIfNeeded -> path = " + path); }
|
||||
|
||||
let splittedPath = path.split("/");
|
||||
|
||||
let partialPath = '';
|
||||
let separator = "";
|
||||
|
||||
let i = 0;
|
||||
loop();
|
||||
|
||||
function loop() {
|
||||
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] createFolderIfNeeded -> path = " + path); }
|
||||
|
||||
partialPath = partialPath + separator + splittedPath[i];
|
||||
separator = "/";
|
||||
|
||||
i++;
|
||||
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] createFolderIfNeeded -> partialPath = " + partialPath); }
|
||||
|
||||
azureFileStorage.createFolder(partialPath, checkLoop);
|
||||
|
||||
function checkLoop(err) {
|
||||
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] createFolderIfNeeded -> loop -> checkLoop -> Entering function."); }
|
||||
|
||||
if (err.result !== global.DEFAULT_OK_RESPONSE.result) {
|
||||
logger.write(MODULE_NAME, "[ERROR] createFolderIfNeeded -> loop -> checkLoop -> err = "+ err.stack);
|
||||
callBackFunction(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (i === splittedPath.length) {
|
||||
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE);
|
||||
|
||||
} else {
|
||||
|
||||
loop();
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] createFolderIfNeeded -> Error = " + err.message);
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function calculatePresicion(number) {
|
||||
|
||||
for (let i = -10; i <= 10; i++) {
|
||||
if (number < Math.pow(10, i)) {
|
||||
return Math.pow(10, i - 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function pad(str, max) {
|
||||
str = str.toString();
|
||||
return str.length < max ? pad("0" + str, max) : str;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,572 @@
|
||||
exports.newCommons = function newCommons(bot, logger, UTILITIES, FILE_STORAGE) {
|
||||
|
||||
const FULL_LOG = true;
|
||||
const LOG_FILE_CONTENT = false;
|
||||
|
||||
const MODULE_NAME = "Commons";
|
||||
|
||||
let thisObject = {
|
||||
validateDataDependencies: validateDataDependencies,
|
||||
validateOutputDatasets: validateOutputDatasets,
|
||||
inflateDatafiles: inflateDatafiles,
|
||||
dataBuildingProcedure: dataBuildingProcedure,
|
||||
calculationsProcedure: calculationsProcedure,
|
||||
generateFileContent: generateFileContent,
|
||||
writeFile: writeFile
|
||||
};
|
||||
|
||||
let utilities = UTILITIES.newCloudUtilities(logger);
|
||||
let fileStorage = FILE_STORAGE.newFileStorage(logger);
|
||||
|
||||
return thisObject;
|
||||
|
||||
function validateDataDependencies(dataDependencies, callBackFunction) {
|
||||
for (let i = 0; i < dataDependencies.length; i++) {
|
||||
|
||||
let dataDependencyNode = dataDependencies[i]
|
||||
|
||||
/* Basic validations to see if we have everything we need. */
|
||||
if (dataDependencyNode.referenceParent.parentNode.config.singularVariableName === undefined) {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> Product Definition without a Single Variable Name defined. Product Definition = " + JSON.stringify(dataDependencyNode.referenceParent.parentNode));
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
return
|
||||
}
|
||||
if (dataDependencyNode.referenceParent.parentNode.config.pluralVariableName === undefined) {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> Product Definition without a Plural Variable Name defined. Product Definition = " + JSON.stringify(dataDependencyNode.referenceParent.parentNode));
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
return
|
||||
}
|
||||
if (dataDependencyNode.referenceParent.parentNode.record === undefined) {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> Product Definition without a Record Definition. Product Definition = " + JSON.stringify(dataDependencyNode.referenceParent.parentNode));
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
return
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function validateOutputDatasets(outputDatasets, callBackFunction) {
|
||||
for (let i = 0; i < outputDatasets.length; i++) {
|
||||
let outputDatasetNode = outputDatasets[i]
|
||||
|
||||
if (outputDatasetNode.referenceParent === undefined) {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> Output Dataset without Reference Parent. Output Dataset = " + JSON.stringify(outputDatasetNode));
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
return
|
||||
}
|
||||
if (outputDatasetNode.referenceParent.parentNode === undefined) {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> Dataset not a child of a Product Definition. Dataset = " + JSON.stringify(outputDatasetNode.referenceParent));
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
return
|
||||
}
|
||||
if (outputDatasetNode.referenceParent.parentNode.config.singularVariableName === undefined) {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> Product Definition without a Single Variable Name defined. Product Definition = " + JSON.stringify(outputDatasetNode.referenceParent.parentNode));
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
return
|
||||
}
|
||||
if (outputDatasetNode.referenceParent.parentNode.config.pluralVariableName === undefined) {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> Product Definition without a Plural Variable Name defined. Product Definition = " + JSON.stringify(outputDatasetNode.referenceParent.parentNode));
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
return
|
||||
}
|
||||
if (outputDatasetNode.referenceParent.parentNode.record === undefined) {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> Product Definition without a Record Definition. Product Definition = " + JSON.stringify(outputDatasetNode.referenceParent.parentNode));
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
return
|
||||
}
|
||||
if (outputDatasetNode.referenceParent.parentNode.dataBuilding === undefined) {
|
||||
logger.write(MODULE_NAME, "[WARN] start -> Product Definition " + outputDatasetNode.referenceParent.parentNode.name + " without a Data Building Procedure. Product Definition Name = " + outputDatasetNode.referenceParent.parentNode.name);
|
||||
}
|
||||
if (outputDatasetNode.referenceParent.config.codeName === undefined) {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> Dataset witn no codeName defined. Product Dataset = " + JSON.stringify(outputDatasetNode.referenceParent));
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
return
|
||||
}
|
||||
|
||||
if (outputDatasetNode.referenceParent.parentNode === undefined) {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> Dataset not attached to a Product Definition. Dataset = " + JSON.stringify(outputDatasetNode.referenceParent));
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
return
|
||||
}
|
||||
|
||||
if (outputDatasetNode.referenceParent.parentNode.config.codeName === undefined) {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> Product Definition witn no codeName defined. Product Definition = " + JSON.stringify(outputDatasetNode.referenceParent.parentNode));
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
return
|
||||
}
|
||||
|
||||
let botNode = global.FIND_NODE_IN_NODE_MESH(outputDatasetNode, 'Indicator Bot')
|
||||
if (botNode === undefined) {
|
||||
botNode = global.FIND_NODE_IN_NODE_MESH(outputDatasetNode, 'Trading Bot')
|
||||
}
|
||||
if (botNode === undefined) {
|
||||
botNode = global.FIND_NODE_IN_NODE_MESH(outputDatasetNode, 'Learning Bot')
|
||||
}
|
||||
if (botNode === undefined) {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> Product Definition not attached to a Bot. Product Definition = ");
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
return
|
||||
}
|
||||
|
||||
if (botNode.config.codeName === undefined) {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> Bot witn no codeName defined.");
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
return
|
||||
}
|
||||
|
||||
let dataMineNode = global.FIND_NODE_IN_NODE_MESH(outputDatasetNode, 'Data Mine')
|
||||
if (dataMineNode === undefined) {
|
||||
let tradingMineNode = global.FIND_NODE_IN_NODE_MESH(outputDatasetNode, 'Trading Mine')
|
||||
if (tradingMineNode === undefined) {
|
||||
let learningMineNode = global.FIND_NODE_IN_NODE_MESH(outputDatasetNode, 'Learning Mine')
|
||||
if (learningMineNode === undefined) {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> Bot not attached to a Data Mine, Trading Mine or Learning Mine.");
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
return
|
||||
} else {
|
||||
if (tradingMineNode.config.codeName === undefined) {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> Learning Mine witn no codeName defined.");
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (tradingMineNode.config.codeName === undefined) {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> Trading Mine witn no codeName defined.");
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (dataMineNode.config.codeName === undefined) {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> Data Mine witn no codeName defined.");
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function inflateDatafiles(dataFiles, dataDependencies, products, mainDependency, timeFrame) {
|
||||
/*
|
||||
For each dataDependencyNode in our data dependencies, we should have a dataFile containing the records needed as an imput for this process.
|
||||
What we need to do first is transform those records into JSON objects that can be used by user-defined formulas.
|
||||
The first step does that but with the not calculated properties, the second step adds the calculated properties.
|
||||
*/
|
||||
for (let i = 0; i < dataDependencies.length; i++) {
|
||||
|
||||
let dataFile
|
||||
let jsonData // Datafile converted into Json objects
|
||||
let inputData // Includes calculated properties
|
||||
let singularVariableName // name of the variable for this product
|
||||
let recordDefinition
|
||||
|
||||
let dataDependencyNode = dataDependencies[i]
|
||||
dataFile = dataFiles.get(dataDependencyNode.id)
|
||||
|
||||
if (dataFile === undefined) { continue } // When a datafile is not found it might be because we are processing market or daily and at the dependency array there are both types mixed up.
|
||||
|
||||
recordDefinition = dataDependencyNode.referenceParent.parentNode.record
|
||||
singularVariableName = dataDependencyNode.referenceParent.parentNode.config.singularVariableName
|
||||
/* Transform the raw data into JSON objects */
|
||||
jsonData = jsonifyDataFile(dataFile, recordDefinition)
|
||||
/* Add the calculated properties */
|
||||
if (dataDependencyNode.referenceParent.parentNode.calculations !== undefined) {
|
||||
inputData = calculationsProcedure(jsonData, recordDefinition, dataDependencyNode.referenceParent.parentNode.calculations, singularVariableName, timeFrame)
|
||||
} else {
|
||||
inputData = jsonData
|
||||
}
|
||||
products[dataDependencyNode.referenceParent.parentNode.config.pluralVariableName] = inputData
|
||||
|
||||
/* The main dependency is defined as the first dependency processed. */
|
||||
if (mainDependency.records === undefined) {
|
||||
mainDependency.records = inputData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function jsonifyDataFile(dataFile, recordDefinition) {
|
||||
|
||||
/*
|
||||
This function has as an input the raw data on files and creates with it an array of JSON objects
|
||||
with not calculated properties for later being consumed by Formulas
|
||||
*/
|
||||
|
||||
let jsonifiedArray = []
|
||||
let previous;
|
||||
|
||||
for (let i = 0; i < dataFile.length; i++) {
|
||||
|
||||
let record = {}
|
||||
for (let j = 0; j < recordDefinition.properties.length; j++) {
|
||||
let property = recordDefinition.properties[j]
|
||||
if (property.config.isCalculated !== true) {
|
||||
record[property.config.codeName] = dataFile[i][j]
|
||||
}
|
||||
}
|
||||
|
||||
record.previous = previous;
|
||||
jsonifiedArray.push(record);
|
||||
previous = record;
|
||||
}
|
||||
|
||||
return jsonifiedArray
|
||||
}
|
||||
|
||||
function calculationsProcedure(jsonArray, recordDefinition, calculationsProcedure, variableName, timeFrame) {
|
||||
|
||||
/*
|
||||
This function has as an input an array of JSON objects, and it adds calculated properties to
|
||||
complete the set of properties that will be available for Formulas.
|
||||
*/
|
||||
|
||||
let system = { // These are the available system variables to be used in User Code and Formulas
|
||||
timeFrame: timeFrame,
|
||||
ONE_DAY_IN_MILISECONDS: global.ONE_DAY_IN_MILISECONDS
|
||||
}
|
||||
let variable = {} // This is the structure where the user will define its own variables that will be shared across different code blocks and formulas.
|
||||
let results = []
|
||||
|
||||
/* This is Initialization Code */
|
||||
if (calculationsProcedure.initialization !== undefined) {
|
||||
if (calculationsProcedure.initialization.javascriptCode !== undefined) {
|
||||
try {
|
||||
eval(calculationsProcedure.initialization.javascriptCode.code)
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] calculationsProcedure -> initialization -> Error executing User Code. Error = " + err.stack)
|
||||
logger.write(MODULE_NAME, "[ERROR] calculationsProcedure -> initialization -> Error executing User Code. Code = " + calculationsProcedure.initialization.javascriptCode.code);
|
||||
throw ("Error Executing User Code.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* This is Initialization Code */
|
||||
if (calculationsProcedure.loop !== undefined) {
|
||||
if (calculationsProcedure.loop.javascriptCode !== undefined) {
|
||||
for (let index = 0; index < jsonArray.length; index++) {
|
||||
|
||||
let product = {}
|
||||
product[variableName] = jsonArray[index]
|
||||
|
||||
/* This is Loop Code */
|
||||
try {
|
||||
eval(calculationsProcedure.loop.javascriptCode.code)
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] calculationsProcedure -> loop -> Error executing User Code. Error = " + err.stack)
|
||||
logger.write(MODULE_NAME, "[ERROR] calculationsProcedure -> loop -> Error executing User Code. product = " + JSON.stringify(product))
|
||||
logger.write(MODULE_NAME, "[ERROR] calculationsProcedure -> loop -> Error executing User Code. Code = " + calculationsProcedure.loop.javascriptCode.code);
|
||||
throw ("Error Executing User Code.")
|
||||
}
|
||||
|
||||
/* For each calculated property we apply its formula*/
|
||||
for (let j = 0; j < recordDefinition.properties.length; j++) {
|
||||
let property = recordDefinition.properties[j]
|
||||
if (property.config.isCalculated === true) {
|
||||
if (property.formula !== undefined) {
|
||||
if (property.formula.code !== undefined) {
|
||||
try {
|
||||
let newValue = eval(property.formula.code)
|
||||
let currentRecord = product[variableName]
|
||||
currentRecord[property.config.codeName] = newValue
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] calculationsProcedure -> loop -> formula -> Error executing User Code. Error = " + err.stack)
|
||||
logger.write(MODULE_NAME, "[ERROR] calculationsProcedure -> loop -> formula -> Error executing User Code. product = " + JSON.stringify(product))
|
||||
logger.write(MODULE_NAME, "[ERROR] calculationsProcedure -> loop -> formula -> Error executing User Code. Code = " + property.formula.code);
|
||||
throw ("Error Executing User Code.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Adding the new element to the resulting array */
|
||||
results.push(product[variableName]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
function dataBuildingProcedure(
|
||||
products,
|
||||
mainDependency,
|
||||
recordDefinition,
|
||||
dataBuildingProcedure,
|
||||
variableName,
|
||||
productName,
|
||||
timeFrame,
|
||||
timeFrameLabel,
|
||||
resultsWithIrregularPeriods,
|
||||
interExecutionMemory,
|
||||
processingDailyFiles,
|
||||
currentDay
|
||||
) {
|
||||
|
||||
/*
|
||||
This function has as an input the products object, with all the information
|
||||
of all products calculated so far by the process. Based on that information
|
||||
the function will evaluate user supplied code and formulas in order to build
|
||||
a new set of information.
|
||||
*/
|
||||
|
||||
let lastInstantOfTheDay
|
||||
let yesterday = {}
|
||||
let system = { // These are the available system variables to be used in User Code and Formulas
|
||||
timeFrame: timeFrame,
|
||||
ONE_DAY_IN_MILISECONDS: global.ONE_DAY_IN_MILISECONDS,
|
||||
ONE_MIN_IN_MILISECONDS: global.ONE_MIN_IN_MILISECONDS
|
||||
}
|
||||
let variable = {} // This is the structure where the user will define its own variables that will be shared across different code blocks and formulas.
|
||||
let results = []
|
||||
|
||||
/*
|
||||
We are going to build a map structure so that users can easily get the current object of
|
||||
each data dependency based on the current element of the main data dependency.
|
||||
We will use as key for these maps the begin and end property, meaning only elements with exactly
|
||||
the same begin and end property will be matched.
|
||||
*/
|
||||
|
||||
const PROPERTIES_COUNT = Object.keys(products).length
|
||||
|
||||
let dataDependencies = {}
|
||||
if (PROPERTIES_COUNT > 1) { // only if we have secondary data dependencies.
|
||||
for (let i = 1; i < PROPERTIES_COUNT; i++) {
|
||||
let dataDependecyArray = Object.values(products)[i]
|
||||
let dataDependencyName = Object.keys(products)[i]
|
||||
let dataDependencyMap = new Map()
|
||||
for (let j = 0; j < dataDependecyArray.length; j++) {
|
||||
let record = dataDependecyArray[j]
|
||||
let key = record.begin.toString() + '-' + record.end.toString()
|
||||
dataDependencyMap.set(key, record)
|
||||
}
|
||||
dataDependencies[dataDependencyName] = dataDependencyMap
|
||||
}
|
||||
}
|
||||
|
||||
function getElement(dependencyName, currentRecordPrimaryDataDependency) {
|
||||
let key = currentRecordPrimaryDataDependency.begin.toString() + '-' + currentRecordPrimaryDataDependency.end.toString()
|
||||
return dataDependencies[dependencyName].get(key)
|
||||
}
|
||||
|
||||
/* This is Initialization Code */
|
||||
if (dataBuildingProcedure.initialization !== undefined) {
|
||||
if (dataBuildingProcedure.initialization.javascriptCode !== undefined) {
|
||||
try {
|
||||
eval(dataBuildingProcedure.initialization.javascriptCode.code)
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] dataBuildingProcedure -> initialization -> Error executing User Code. Error = " + err.stack)
|
||||
logger.write(MODULE_NAME, "[ERROR] dataBuildingProcedure -> initialization -> Error executing User Code. Code = " + dataBuildingProcedure.initialization.javascriptCode.code);
|
||||
throw ("Error Executing User Code.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (processingDailyFiles) {
|
||||
/* Initialization of Last Instance */
|
||||
lastInstantOfTheDay = currentDay.valueOf() + global.ONE_DAY_IN_MILISECONDS - 1;
|
||||
|
||||
if (interExecutionMemory[productName] === undefined) {
|
||||
/* The first time the intialization variables goes to the Inter Execution Memory. */
|
||||
interExecutionMemory[productName] = {}
|
||||
interExecutionMemory[productName].variable = JSON.parse(JSON.stringify(variable))
|
||||
}
|
||||
else {
|
||||
/* We override the initialization, since the valid stuff is already at the Inter Execution Memory */
|
||||
variable = JSON.parse(JSON.stringify(interExecutionMemory[productName].variable))
|
||||
}
|
||||
}
|
||||
|
||||
/* This is Initialization Code */
|
||||
if (dataBuildingProcedure.loop !== undefined) {
|
||||
if (dataBuildingProcedure.loop.javascriptCode !== undefined) {
|
||||
let lastRecord
|
||||
for (let index = 0; index < mainDependency.records.length; index++) {
|
||||
|
||||
let record = {
|
||||
previous: mainDependency.records[index - 1],
|
||||
current: mainDependency.records[index],
|
||||
next: mainDependency.records[index + 1]
|
||||
}
|
||||
lastRecord = record
|
||||
let product = {}
|
||||
product[variableName] = {}
|
||||
|
||||
/* This is Loop Code */
|
||||
try {
|
||||
eval(dataBuildingProcedure.loop.javascriptCode.code)
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] dataBuildingProcedure -> loop -> Error executing User Code. Error = " + err.stack)
|
||||
logger.write(MODULE_NAME, "[ERROR] dataBuildingProcedure -> loop -> Error executing User Code. product = " + JSON.stringify(product))
|
||||
logger.write(MODULE_NAME, "[ERROR] dataBuildingProcedure -> loop -> Error executing User Code. Code = " + dataBuildingProcedure.loop.javascriptCode.code);
|
||||
throw ("Error Executing User Code.")
|
||||
}
|
||||
|
||||
/* For each non-calculated property we apply its formula*/
|
||||
for (let j = 0; j < recordDefinition.properties.length; j++) {
|
||||
let property = recordDefinition.properties[j]
|
||||
if (property.config.isCalculated !== true) {
|
||||
if (property.formula !== undefined) {
|
||||
if (property.formula.code !== undefined) {
|
||||
try {
|
||||
let newValue = eval(property.formula.code)
|
||||
let currentRecord = product[variableName]
|
||||
currentRecord[property.config.codeName] = newValue
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] dataBuildingProcedure -> loop -> formula -> Error executing User Code. Error = " + err.stack)
|
||||
logger.write(MODULE_NAME, "[ERROR] dataBuildingProcedure -> loop -> formula -> Error executing User Code. product = " + JSON.stringify(product))
|
||||
logger.write(MODULE_NAME, "[ERROR] dataBuildingProcedure -> loop -> formula -> Error executing User Code. Code = " + property.formula.code);
|
||||
throw ("Error Executing User Code.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* If within the code the new data structure was not yet pushed to the results array, we do it */
|
||||
if (resultsWithIrregularPeriods !== true) {
|
||||
/* Adding the new element to the resulting array */
|
||||
results.push(product[variableName]);
|
||||
}
|
||||
|
||||
/* At the last instant of yesterday, we update the yesterday object. */
|
||||
if (processingDailyFiles) {
|
||||
if (record.current.end + 1 === currentDay.valueOf()) {
|
||||
yesterday.variable = JSON.parse(JSON.stringify(variable))
|
||||
}
|
||||
}
|
||||
}
|
||||
/*
|
||||
Before returning we need to see if we have to record some of our counters at the interExecutionMemory.
|
||||
To do that, the condition to be met is that this execution must include all candles of the current day.
|
||||
*/
|
||||
if (processingDailyFiles) {
|
||||
if (lastRecord.current.end === lastInstantOfTheDay && yesterday.variable !== undefined) {
|
||||
interExecutionMemory[productName] = {}
|
||||
interExecutionMemory[productName].variable = JSON.parse(JSON.stringify(yesterday.variable))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
function generateFileContent(records, recordDefinition, resultsWithIrregularPeriods, processingDailyFiles, currentDay, callBackFunction) {
|
||||
|
||||
try {
|
||||
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> generateFileContent -> Entering function."); }
|
||||
|
||||
let fileContent = "";
|
||||
let recordSeparator = "";
|
||||
|
||||
for (let i = 0; i < records.length; i++) {
|
||||
let record = records[i];
|
||||
|
||||
if (processingDailyFiles === true) {
|
||||
|
||||
if (resultsWithIrregularPeriods === true) {
|
||||
/*
|
||||
Here we have an special problem that occurs when an object spans several time peridos. If not taken care of
|
||||
it can happen that the object gets splitted between 2 days, which we dont want since it would loose some of
|
||||
its properties.
|
||||
|
||||
To solve this issue, we wont save objects which ends at the last candle of the day, because we will save it
|
||||
at the next day, in whole, even if it starts in the previous day.
|
||||
*/
|
||||
|
||||
let lastInstantOdDay = currentDay.valueOf() + global.ONE_DAY_IN_MILISECONDS - 1;
|
||||
|
||||
if (record.end < currentDay.valueOf() - 1) { continue; }
|
||||
if (record.end === lastInstantOdDay) { continue; }
|
||||
} else {
|
||||
/*
|
||||
The normal situation is that objects are respecting the time size of its main dependency, in which case we only
|
||||
care of not adding objects of the previous day we are currently processing (while on daily files)
|
||||
*/
|
||||
if (record.end < currentDay.valueOf()) { continue; }
|
||||
}
|
||||
}
|
||||
|
||||
fileContent = fileContent + recordSeparator + '['
|
||||
let propertySeparator = ""
|
||||
for (let j = 0; j < recordDefinition.properties.length; j++) {
|
||||
let property = recordDefinition.properties[j]
|
||||
if (property.config.isCalculated !== true) {
|
||||
fileContent = fileContent + propertySeparator
|
||||
if (property.config.isString === true) {
|
||||
fileContent = fileContent + '"'
|
||||
}
|
||||
fileContent = fileContent + record[property.config.codeName]
|
||||
if (property.config.isString === true) {
|
||||
fileContent = fileContent + '"'
|
||||
}
|
||||
if (propertySeparator === "") { propertySeparator = ","; }
|
||||
}
|
||||
}
|
||||
fileContent = fileContent + ']'
|
||||
if (recordSeparator === "") { recordSeparator = ","; }
|
||||
}
|
||||
fileContent = "[" + fileContent + "]";
|
||||
return fileContent
|
||||
}
|
||||
catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> generateFileContent -> err = " + err.stack);
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
}
|
||||
}
|
||||
|
||||
function writeFile(contextSummary, fileContent, anotherFileWritten, processingDailyFiles, timeFrameLabel, currentDay, callBackFunction) {
|
||||
|
||||
try {
|
||||
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> writeFile -> Entering function."); }
|
||||
|
||||
let market = bot.market;
|
||||
let fileName = 'Data.json';
|
||||
let dateForPath = ''
|
||||
|
||||
if (processingDailyFiles === true) {
|
||||
dateForPath = "/" + currentDay.getUTCFullYear() + '/' + utilities.pad(currentDay.getUTCMonth() + 1, 2) + '/' + utilities.pad(currentDay.getUTCDate(), 2);
|
||||
}
|
||||
|
||||
let filePathRoot = 'Project/' + contextSummary.project + "/" + contextSummary.mineType + "/" + contextSummary.dataMine + "/" + contextSummary.bot + '/' + bot.exchange + "/" + bot.market.baseAsset + "-" + bot.market.quotedAsset
|
||||
let filePath = filePathRoot + "/Output/" + contextSummary.product + "/" + contextSummary.dataset + "/" + timeFrameLabel + dateForPath;
|
||||
filePath += '/' + fileName
|
||||
|
||||
fileStorage.createTextFile(filePath, fileContent + '\n', onFileCreated);
|
||||
|
||||
function onFileCreated(err) {
|
||||
|
||||
try {
|
||||
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> writeFile -> onFileCreated -> Entering function."); }
|
||||
if (LOG_FILE_CONTENT === true) { logger.write(MODULE_NAME, "[INFO] start -> writeFile -> onFileCreated -> fileContent = " + fileContent); }
|
||||
|
||||
if (err.result !== global.DEFAULT_OK_RESPONSE.result) {
|
||||
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> writeFile -> onFileCreated -> err = " + err.stack);
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> writeFile -> onFileCreated -> filePath = " + filePath);
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> writeFile -> onFileCreated -> market = " + market.baseAsset + "_" + market.quotedAsset);
|
||||
|
||||
callBackFunction(err);
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
anotherFileWritten();
|
||||
|
||||
}
|
||||
catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> writeFile -> onFileCreated -> err = " + err.stack);
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> writeFile -> err = " + err.stack);
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
exports.newDataDependencies = function newDataDependencies(BOT, logger, DATA_SET) {
|
||||
|
||||
const MODULE_NAME = "Data Dependencies";
|
||||
|
||||
let bot = BOT
|
||||
|
||||
let thisObject = {
|
||||
nodeArray: undefined,
|
||||
dataSetsModulesArray: [],
|
||||
isItADepenency: isItADepenency,
|
||||
initialize: initialize,
|
||||
finalize: finalize
|
||||
};
|
||||
|
||||
let filter = new Map()
|
||||
|
||||
return thisObject;
|
||||
|
||||
function initialize(callBackFunction) {
|
||||
try {
|
||||
/* Basic Valdidations */
|
||||
if (bot.processNode.referenceParent.processDependencies !== undefined) {
|
||||
thisObject.nodeArray = global.NODE_BRANCH_TO_ARRAY(bot.processNode.referenceParent.processDependencies, 'Data Dependency')
|
||||
if (thisObject.nodeArray.length === 0) {
|
||||
logger.write(MODULE_NAME, "[ERROR] initialize -> onInitilized -> It is not possible to not have data dependencies at all.");
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE)
|
||||
return
|
||||
}
|
||||
/*
|
||||
We will filter ourt declared dependencies that are not present in the workspace.
|
||||
This will allow the user to have less Data Mines loaded at the workspace
|
||||
that the ones that a Trading Mine depends on.
|
||||
*/
|
||||
thisObject.nodeArray = global.FILTER_OUT_NODES_WITHOUT_REFERENCE_PARENT_FROM_NODE_ARRAY(thisObject.nodeArray)
|
||||
|
||||
} else {
|
||||
logger.write(MODULE_NAME, "[ERROR] initialize -> onInitilized -> It is not possible not to have process dependencies, which means not data dependencies.");
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE)
|
||||
return
|
||||
}
|
||||
|
||||
if (thisObject.nodeArray.length === 0) {
|
||||
|
||||
// We allow old indicators not to declare their data dependencies.
|
||||
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Session based dependency filters */
|
||||
if (bot.DEPENDENCY_FILTER !== undefined) {
|
||||
for (let i = 0; i < bot.DEPENDENCY_FILTER.length; i++) {
|
||||
let key = bot.DEPENDENCY_FILTER[i]
|
||||
filter.set(key, true)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
For each dependency declared at the nodeArray, we will initialize a DataSet as part of this initialization process.
|
||||
*/
|
||||
let alreadyCalledBack = false
|
||||
let addCount = 0
|
||||
let skipCount = 0
|
||||
|
||||
/*
|
||||
The current nodeArray that we have includes all the dependencies daclared in the Data Mine | Trading Mine | Learning Mine
|
||||
process dependencies, minus the ones without reference parent, meaning that references Data Mines that are
|
||||
not present at the workspace. From all the remaining dependencies there will be others that we need to
|
||||
filter out, and they are the ones which the user does not have a data product anywhere on the network where
|
||||
this task is running, that is storing the data associated to this dependency. The way we use to filter this
|
||||
out is by analizyng the response of the initialization call to the DataSet module. If it did not fail but
|
||||
it was not initialized, then we will assume this happened because the data could not be located and we will
|
||||
assume the user did this on purpose and we will filter out this dependency by not putting it on this new array.
|
||||
*/
|
||||
let newNodeArray = []
|
||||
|
||||
for (let i = 0; i < thisObject.nodeArray.length; i++) {
|
||||
let dataSetModule = DATA_SET.newDataSet(BOT, logger);
|
||||
dataSetModule.initialize(thisObject.nodeArray[i], onInitilized);
|
||||
|
||||
function onInitilized(err, wasInitialized) {
|
||||
|
||||
if (err.result !== global.DEFAULT_OK_RESPONSE.result) {
|
||||
logger.write(MODULE_NAME, "[ERROR] initialize -> onInitilized -> err = " + JSON.stringify(err));
|
||||
|
||||
alreadyCalledBack = true;
|
||||
callBackFunction(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (wasInitialized === true) {
|
||||
addCount++;
|
||||
newNodeArray.push(thisObject.nodeArray[i])
|
||||
addDataSet()
|
||||
} else {
|
||||
skipCount++
|
||||
}
|
||||
checkIfWeAreDone()
|
||||
}
|
||||
|
||||
function addDataSet() {
|
||||
thisObject.dataSetsModulesArray.push(dataSetModule);
|
||||
}
|
||||
|
||||
function checkIfWeAreDone() {
|
||||
if (addCount + skipCount === thisObject.nodeArray.length) {
|
||||
if (alreadyCalledBack === false) {
|
||||
alreadyCalledBack = true
|
||||
thisObject.nodeArray = newNodeArray
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] initialize -> err = " + err.stack);
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
}
|
||||
}
|
||||
|
||||
function finalize() {
|
||||
for (let i = 0; i < thisObject.dataSetsModulesArray.length; i++) {
|
||||
let dataSetModule = thisObject.dataSetsModulesArray[i]
|
||||
dataSetModule.finalize()
|
||||
}
|
||||
thisObject.dataSetsModulesArray = undefined
|
||||
filter = undefined
|
||||
bot = undefined
|
||||
}
|
||||
|
||||
function isItADepenency(timeFrame, product) {
|
||||
let key = timeFrame + '-' + product
|
||||
|
||||
return filter.get(key)
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,186 @@
|
||||
exports.newDataSet = function newDataSet(BOT, logger) {
|
||||
|
||||
const MODULE_NAME = "Dataset";
|
||||
|
||||
let bot = BOT;
|
||||
|
||||
let thisObject = {
|
||||
node: undefined,
|
||||
networkNode: undefined,
|
||||
initialize: initialize,
|
||||
finalize: finalize,
|
||||
getTextFile: getTextFile,
|
||||
createTextFile: createTextFile
|
||||
};
|
||||
|
||||
/* Storage account to be used here. */
|
||||
|
||||
const FILE_STORAGE = require('./FileStorage.js');
|
||||
let fileStorage
|
||||
|
||||
return thisObject;
|
||||
|
||||
function initialize(dataDependency, callBackFunction) {
|
||||
|
||||
try {
|
||||
thisObject.node = dataDependency.referenceParent;
|
||||
logger.fileName = MODULE_NAME
|
||||
|
||||
/* Some very basic validations that we have all the information needed. */
|
||||
if (thisObject.node === undefined) {
|
||||
logger.write(MODULE_NAME, "[ERROR] initialize -> Data Dependency without Reference Parent -> dataDependency = " + JSON.stringify(dataDependency));
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
return
|
||||
}
|
||||
|
||||
logger.fileName = MODULE_NAME + "." + thisObject.node.type + "." + thisObject.node.name + "." + thisObject.node.id;
|
||||
|
||||
if (thisObject.node.config.codeName === undefined) {
|
||||
logger.write(MODULE_NAME, "[ERROR] initialize -> Dataset witn no codeName defined -> Product Dataset = " + JSON.stringify(thisObject.node));
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
return
|
||||
}
|
||||
|
||||
if (thisObject.node.parentNode === undefined) {
|
||||
logger.write(MODULE_NAME, "[ERROR] initialize -> Dataset not attached to a Product Definition -> Dataset = " + JSON.stringify(thisObject.node));
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
return
|
||||
}
|
||||
|
||||
if (thisObject.node.parentNode.config.codeName === undefined) {
|
||||
logger.write(MODULE_NAME, "[ERROR] initialize -> Product Definition witn no codeName defined -> Product Definition = " + JSON.stringify(thisObject.node.parentNode));
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
return
|
||||
}
|
||||
|
||||
if (thisObject.node.parentNode.parentNode === undefined) {
|
||||
logger.write(MODULE_NAME, "[ERROR] initialize -> Product Definition not attached to a Bot -> Product Definition = " + JSON.stringify(thisObject.node.parentNode));
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
return
|
||||
}
|
||||
|
||||
if (thisObject.node.parentNode.parentNode.config.codeName === undefined) {
|
||||
logger.write(MODULE_NAME, "[ERROR] initialize -> Bot witn no codeName defined. Bot = " + JSON.stringify(thisObject.node.parentNode.parentNode));
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
return
|
||||
}
|
||||
|
||||
if (thisObject.node.parentNode.parentNode.parentNode === undefined) {
|
||||
logger.write(MODULE_NAME, "[ERROR] initialize -> Bot not attached to a Data Mine or Trading Mine. Bot = " + JSON.stringify(thisObject.node.parentNode.parentNode));
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
return
|
||||
}
|
||||
|
||||
if (thisObject.node.parentNode.parentNode.parentNode.config.codeName === undefined) {
|
||||
logger.write(MODULE_NAME, "[ERROR] initialize -> Data Mine witn no codeName defined. Data Mine = " + JSON.stringify(thisObject.node.parentNode.parentNode.parentNode));
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
return
|
||||
}
|
||||
|
||||
/* Now we will see where do we need to fetch this data from. */
|
||||
let network = global.TASK_NETWORK
|
||||
let datasetProductDefinition = thisObject.node.parentNode
|
||||
|
||||
let nodeArray = global.NODE_MESH_TO_PATH_ARRAY(network, datasetProductDefinition.id)
|
||||
let networkNode = global.FIND_NODE_IN_NODE_ARRAY(nodeArray, 'Network Node')
|
||||
|
||||
if (networkNode === undefined) {
|
||||
logger.write(MODULE_NAME, "[WARN] initialize -> Network Node not found.")
|
||||
logger.write(MODULE_NAME, "[WARN] initialize -> Initialization Failed because we could not find where the data of this dataset is located within the network. Check the logs for more info.");
|
||||
logger.write(MODULE_NAME, "[WARN] initialize -> Could not find where " + datasetProductDefinition.name + " for " + bot.exchange + " " + bot.market.baseAsset + "/" + bot.market.quotedAsset + " is stored within the network.");
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE, false);
|
||||
return
|
||||
}
|
||||
|
||||
/* We found where the data is located on the network. */
|
||||
if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { logger.write(MODULE_NAME, "[INFO] initialize -> Retrieving data from " + networkNode.name + " -> host = " + networkNode.config.host + ' -> port = ' + networkNode.config.webPort + '.'); }
|
||||
|
||||
fileStorage = FILE_STORAGE.newFileStorage(logger, networkNode.config.host, networkNode.config.webPort);
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE, true);
|
||||
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] initialize -> err = " + err.stack);
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
}
|
||||
}
|
||||
|
||||
function finalize() {
|
||||
fileStorage = undefined
|
||||
thisObject.networkNode = undefined
|
||||
bot = undefined
|
||||
thisObject = undefined
|
||||
}
|
||||
|
||||
function getTextFile(pFolderPath, pFileName, callBackFunction) {
|
||||
|
||||
try {
|
||||
|
||||
if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { logger.write(MODULE_NAME, "[INFO] getTextFile -> Entering function."); }
|
||||
|
||||
let mine = thisObject.node.parentNode.parentNode.parentNode.config.codeName
|
||||
let mineType = thisObject.node.parentNode.parentNode.parentNode.type.replace(' ', '-')
|
||||
let project = thisObject.node.parentNode.parentNode.parentNode.project
|
||||
let botCodeName = thisObject.node.parentNode.parentNode.config.codeName
|
||||
|
||||
let filePathRoot = 'Project/' + project + "/" + mineType + "/" + mine + "/" + botCodeName + '/' + bot.exchange + "/" + bot.market.baseAsset + "-" + bot.market.quotedAsset
|
||||
let filePath = filePathRoot + "/Output/" + pFolderPath;
|
||||
filePath += '/' + pFileName
|
||||
|
||||
fileStorage.getTextFile(filePath, onFileReceived);
|
||||
|
||||
function onFileReceived(err, text) {
|
||||
|
||||
if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { logger.write(MODULE_NAME, "[INFO] getTextFile -> onFileReceived -> Entering function."); }
|
||||
|
||||
callBackFunction(err, text);
|
||||
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] 'getTextFile' -> err = " + err.stack);
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
}
|
||||
}
|
||||
|
||||
function createTextFile(pFolderPath, pFileName, pFileContent, callBackFunction) {
|
||||
|
||||
try {
|
||||
|
||||
if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { logger.write(MODULE_NAME, "[INFO] createTextFile -> Entering function."); }
|
||||
if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { logger.write(MODULE_NAME, "[INFO] createTextFile -> pFolderPath = " + pFolderPath); }
|
||||
if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { logger.write(MODULE_NAME, "[INFO] createTextFile -> pFileName = " + pFileName); }
|
||||
|
||||
let ownerId = thisObject.node.dataMine + "-" + thisObject.node.bot
|
||||
let botId = bot.dataMine + "-" + bot.codeName
|
||||
|
||||
if (ownerId !== botId) {
|
||||
|
||||
let customErr = {
|
||||
result: global.CUSTOM_FAIL_RESPONSE.result,
|
||||
message: "Only bots owners of a Data Set can create text files on them."
|
||||
};
|
||||
logger.write(MODULE_NAME, "[ERROR] save -> customErr = " + customErr.message);
|
||||
callBackFunction(customErr);
|
||||
return;
|
||||
}
|
||||
|
||||
let filePathRoot = 'Project/' + thisObject.node.project + "/" + thisObject.node.mineType + "/" + thisObject.node.dataMine + "/" + thisObject.node.bot + '/' + bot.exchange + "/" + bot.market.baseAsset + "-" + bot.market.quotedAsset
|
||||
let filePath = filePathRoot + "/Output/" + pFolderPath + '/' + pFileName;
|
||||
|
||||
fileStorage.createTextFile(filePath, pFileContent, onFileCreated);
|
||||
|
||||
function onFileCreated(err) {
|
||||
|
||||
if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { logger.write(MODULE_NAME, "[INFO] createTextFile -> onFileCreated -> Entering function."); }
|
||||
|
||||
callBackFunction(err);
|
||||
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] 'createTextFile' -> err = " + err.stack);
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
@@ -0,0 +1,188 @@
|
||||
exports.newDebugLog = function newDebugLog() {
|
||||
|
||||
const MODULE_NAME = "DebugLog";
|
||||
|
||||
let executionDatetime = "D." + global.EXECUTION_DATETIME.getUTCFullYear() +
|
||||
"." + pad(global.EXECUTION_DATETIME.getUTCMonth() + 1, 2) +
|
||||
"." + pad(global.EXECUTION_DATETIME.getUTCDate(), 2) +
|
||||
".T." + pad(global.EXECUTION_DATETIME.getUTCHours(), 2) +
|
||||
"." + pad(global.EXECUTION_DATETIME.getUTCMinutes(), 2) +
|
||||
"." + pad(global.EXECUTION_DATETIME.getUTCSeconds(), 2);
|
||||
|
||||
let messageId = 0;
|
||||
|
||||
let thisObject = {
|
||||
bot: undefined,
|
||||
write: write,
|
||||
newInternalLoop: newInternalLoop,
|
||||
persist: persist, // This method is executed at the end of each Main Loop.
|
||||
initialize: initialize,
|
||||
finalize: finalize
|
||||
};
|
||||
|
||||
let accumulatedLog = "[";
|
||||
let disableLogging;
|
||||
let internalLoopCounter = -1;
|
||||
|
||||
return thisObject;
|
||||
|
||||
function initialize(pdisableLogging) {
|
||||
|
||||
try {
|
||||
|
||||
disableLogging = pdisableLogging
|
||||
|
||||
} catch (err) {
|
||||
console.log("[ERROR] Debug Log -> initialize -> err = " + err.stack);
|
||||
}
|
||||
}
|
||||
|
||||
function finalize() {
|
||||
persist()
|
||||
}
|
||||
|
||||
function newInternalLoop(pBot, pProcess, date, percentage) {
|
||||
if (percentage === undefined) {
|
||||
percentage = ""
|
||||
} else {
|
||||
percentage = percentage.toFixed(2) + " %"
|
||||
}
|
||||
|
||||
if (date === undefined) { date = thisObject.bot.processDatetime }
|
||||
date = date.getUTCFullYear() + '-' + strPad(date.getUTCMonth() + 1, 2, "0") + '-' + strPad(date.getUTCDate(), 2, "0");
|
||||
|
||||
console.log(new Date().toISOString() + " " + strPad(thisObject.bot.exchange, 20) + " " + strPad(thisObject.bot.market.baseAsset + '/' + thisObject.bot.market.quotedAsset, 10) + " " + strPad(pBot, 30) + " " + strPad(pProcess, 30)
|
||||
+ " Internal Loop # " + strPad(internalLoopCounter + 1, 8) + " " + strPad(date, 30) + " " + strPad(percentage, 10))
|
||||
|
||||
persist();
|
||||
|
||||
function strPad(str, max, fill) {
|
||||
if (fill === undefined) { fill = " " }
|
||||
str = str.toString();
|
||||
return str.length < max ? strPad(fill + str, max) : str;
|
||||
}
|
||||
}
|
||||
|
||||
function persist() {
|
||||
|
||||
/* Here we actually write the content of the in-memory log to a file */
|
||||
|
||||
try {
|
||||
if (accumulatedLog === "[") { return } // nothing to persist at the moment.
|
||||
|
||||
internalLoopCounter++;
|
||||
|
||||
let contentToPersist = accumulatedLog;
|
||||
accumulatedLog = "[";
|
||||
|
||||
const FILE_STORAGE = require('./FileStorage.js');
|
||||
let fileStorage = FILE_STORAGE.newFileStorage();
|
||||
|
||||
let filePath = thisObject.bot.filePathRoot + "/Logs/" + thisObject.bot.process + "/"
|
||||
|
||||
if (thisObject.bot.TRADING_SESSION !== undefined) {
|
||||
filePath = filePath + thisObject.bot.TRADING_SESSION.folderName + "/" + executionDatetime;
|
||||
} else {
|
||||
if (thisObject.bot.LEARNING_SESSION !== undefined) {
|
||||
filePath = filePath + thisObject.bot.LEARNING_SESSION.folderName + "/" + executionDatetime;
|
||||
} else {
|
||||
filePath = filePath + executionDatetime;
|
||||
}
|
||||
}
|
||||
|
||||
if (thisObject.bot.debug.year !== undefined) {
|
||||
|
||||
filePath = filePath + "/" + thisObject.bot.debug.year + "/" + thisObject.bot.debug.month;
|
||||
}
|
||||
|
||||
let fileName;
|
||||
if (internalLoopCounter >= 0) {
|
||||
fileName = "Loop." + pad(thisObject.bot.loopCounter, 8) + "." + pad(internalLoopCounter, 4) + ".json";
|
||||
} else {
|
||||
fileName = "Loop." + pad(thisObject.bot.loopCounter, 8) + ".json";
|
||||
}
|
||||
|
||||
writeLog();
|
||||
|
||||
/* This is the implementation of the mechanism to auto-mantain logs. */
|
||||
if (contentToPersist.indexOf('[ERROR]') < 0 && contentToPersist.indexOf('[PERSIST]') < 0 && contentToPersist.indexOf('[IMPORTANT]') < 0) {
|
||||
thisObject.bot.LOGS_TO_DELETE_QUEUE.push(filePath + '/' + fileName)
|
||||
}
|
||||
if (thisObject.bot.LOGS_TO_DELETE_QUEUE.length > thisObject.bot.DELETE_QUEUE_SIZE) {
|
||||
let fileToDelete = thisObject.bot.LOGS_TO_DELETE_QUEUE[0]
|
||||
thisObject.bot.LOGS_TO_DELETE_QUEUE.splice(0, 1)
|
||||
/* Will delete this file only if it does not contains ERROR inside. */
|
||||
let fileContent = fileStorage.getTextFile(fileToDelete, onGetFile, true)
|
||||
function onGetFile(err, fileContent) {
|
||||
if (err.result === global.DEFAULT_OK_RESPONSE.result) {
|
||||
if (fileContent.indexOf("ERROR") < 0) {
|
||||
/* Logs will only be deleted when they contain no ERROR in them. */
|
||||
fileStorage.deleteTextFile(fileToDelete);
|
||||
}
|
||||
} else {
|
||||
fileStorage.deleteTextFile(fileToDelete);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function writeLog() {
|
||||
|
||||
fileStorage.createTextFile(filePath + '/' + fileName, contentToPersist + '\r\n' + "]", onFileCreated, undefined, true);
|
||||
function onFileCreated(err) {
|
||||
if (err.result !== global.DEFAULT_OK_RESPONSE.result) {
|
||||
console.log("[ERROR] DebugLog -> persist -> onInizialized -> onFileCreated -> err = " + err.message);
|
||||
setTimeout(writeLog, 10000); // Lets retry until we make it.
|
||||
return;
|
||||
}
|
||||
contentToPersist = "";
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("[ERROR] DebugLog -> persist -> err = " + err.stack);
|
||||
console.log("[ERROR] DebugLog -> persist -> onInizialized -> contentToPersist = " + contentToPersist);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function write(pModule, pMessage) {
|
||||
try {
|
||||
if (disableLogging === true) { return; }
|
||||
|
||||
if (thisObject.bot === undefined) { return; }
|
||||
|
||||
if (pModule === "") { // For debugging purposes
|
||||
console.log(pMessage)
|
||||
}
|
||||
|
||||
let newDate = new Date();
|
||||
newDate = newDate.toISOString();
|
||||
|
||||
messageId++;
|
||||
|
||||
/* When writting one file for all modules we use this. */
|
||||
|
||||
let message = "['" + newDate + "'," + messageId + ",'" + pModule + "','" + pMessage + "']"
|
||||
let logLine = '\r\n' + message;
|
||||
|
||||
if (process.env.CONSOLE_LOG === "true" || message.indexOf("ERROR") > 0) {
|
||||
let key = ''
|
||||
if (thisObject.bot) {
|
||||
key = thisObject.bot.dataMine + '-' + thisObject.bot.codeName + '-' + thisObject.bot.process
|
||||
}
|
||||
console.log('*********** ' + message + ' @ ' + key)
|
||||
}
|
||||
|
||||
accumulatedLog = accumulatedLog + logLine;
|
||||
|
||||
} catch (err) {
|
||||
console.log("[ERROR] DebugLog -> write -> err = " + err.stack);
|
||||
}
|
||||
}
|
||||
|
||||
function pad(str, max) {
|
||||
str = str.toString();
|
||||
return str.length < max ? pad("0" + str, max) : str;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
exports.newEventsServerClient = function newEventsServerClient(host, port) {
|
||||
|
||||
const MODULE = "Task Server "
|
||||
|
||||
const INFO_LOG = false
|
||||
|
||||
let thisObject = {
|
||||
initialize: initialize,
|
||||
finalize: finalize,
|
||||
createEventHandler: createEventHandler,
|
||||
deleteEventHandler: deleteEventHandler,
|
||||
listenToEvent: listenToEvent,
|
||||
stopListening: stopListening,
|
||||
raiseEvent: raiseEvent
|
||||
}
|
||||
|
||||
let eventListeners = new Map()
|
||||
let responseWaiters = new Map()
|
||||
|
||||
let WEB_SOCKETS_CLIENT
|
||||
const WEB_SOCKET = require('ws')
|
||||
|
||||
if (host === undefined) {
|
||||
host = 'localhost'
|
||||
}
|
||||
if (port === undefined) {
|
||||
port = process.env.WEB_SOCKETS_INTERFACE_PORT
|
||||
}
|
||||
|
||||
let messageCounter = 0
|
||||
let commandCounter = 0
|
||||
|
||||
return thisObject
|
||||
|
||||
function initialize(callBackFunction) {
|
||||
|
||||
setuptWebSockets(callBackFunction)
|
||||
|
||||
}
|
||||
|
||||
function setuptWebSockets(callBackFunction) {
|
||||
try {
|
||||
|
||||
if (INFO_LOG === true) {
|
||||
console.log('setuptWebSockets at ' + host + ':' + port)
|
||||
}
|
||||
WEB_SOCKETS_CLIENT = new WEB_SOCKET('ws://' + host + ':' + port )
|
||||
|
||||
WEB_SOCKETS_CLIENT.onerror = err => {
|
||||
console.log('[ERROR] Task Server -> Event Server Client -> setuptWebSockets -> On connection error -> error = ' + err.stack)
|
||||
console.log('[ERROR] This could mean that the port '+ port +' is taken by some other app running at your system. To resolve this issue, please pick another port number and change it at the .ENV file inside the Superalgos folder AND at the .ENV file inside the TaskServer folder. After that run the app again. ')
|
||||
}
|
||||
WEB_SOCKETS_CLIENT.onopen = () => {
|
||||
try {
|
||||
if (INFO_LOG === true) {
|
||||
console.log('Websocket connection opened.')
|
||||
}
|
||||
|
||||
if (callBackFunction !== undefined) {
|
||||
callBackFunction()
|
||||
}
|
||||
} catch(err) {
|
||||
console.log('[ERROR] Task Server -> Event Server Client -> setuptWebSockets -> onopen -> err = ' + err.stack)
|
||||
}
|
||||
}
|
||||
WEB_SOCKETS_CLIENT.onmessage = e => {
|
||||
try {
|
||||
if (INFO_LOG === true) {
|
||||
console.log('Websocket Message Received: ' + e.data.substring(0, 1000))
|
||||
}
|
||||
|
||||
let message = JSON.parse(e.data)
|
||||
|
||||
if (message.action === 'Event Raised') {
|
||||
let key
|
||||
if (message.callerId !== undefined) {
|
||||
key = message.eventHandlerName + '-' + message.eventType + '-' + message.callerId
|
||||
} else {
|
||||
key = message.eventHandlerName + '-' + message.eventType
|
||||
}
|
||||
let handler = eventListeners.get(key)
|
||||
if (handler) {
|
||||
handler.callBack(message)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (message.action === 'Event Server Response') {
|
||||
let handler = responseWaiters.get(message.callerId)
|
||||
if (handler) {
|
||||
handler(message)
|
||||
}
|
||||
return
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('[ERROR] Task Server -> Event Server Client -> setuptWebSockets -> onmessage -> err = ' + err.stack)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('[ERROR] Task Server -> Event Server Client -> setuptWebSockets -> err = ' + err.stack)
|
||||
}
|
||||
}
|
||||
|
||||
function finalize() {
|
||||
/* Before disconnecting we will forze all eventListeners to stop listening. */
|
||||
const eventListenersArray = [...eventListeners.values()]
|
||||
for (let i = 0; i < eventListenersArray.length; i++) {
|
||||
let handler = eventListenersArray[i]
|
||||
let eventCommand = {
|
||||
action: 'stopListening',
|
||||
eventHandlerName: handler.eventHandlerName,
|
||||
eventType: handler.eventType,
|
||||
callerId: handler.callerId
|
||||
}
|
||||
sendCommand(eventCommand)
|
||||
}
|
||||
WEB_SOCKETS_CLIENT.close();
|
||||
}
|
||||
|
||||
function sendCommand(command, responseCallBack, eventsCallBack) {
|
||||
try {
|
||||
if (command.action === 'listenToEvent') {
|
||||
let key
|
||||
if (command.callerId) {
|
||||
key = command.eventHandlerName + '-' + command.eventType + '-' + command.callerId
|
||||
} else {
|
||||
key = command.eventHandlerName + '-' + command.eventType
|
||||
}
|
||||
let handler = {
|
||||
eventHandlerName: command.eventHandlerName,
|
||||
eventType: command.eventType,
|
||||
callerId: command.callerId,
|
||||
callBack: eventsCallBack
|
||||
}
|
||||
eventListeners.set(key, handler)
|
||||
}
|
||||
if (command.callerId && responseCallBack) {
|
||||
responseWaiters.set(command.callerId, responseCallBack)
|
||||
}
|
||||
|
||||
if (WEB_SOCKETS_CLIENT.readyState === 1) { // 1 means connected and ready.
|
||||
|
||||
WEB_SOCKETS_CLIENT.send("Task Server" + "|*|" + "|*|" + JSON.stringify(command))
|
||||
|
||||
} else {
|
||||
console.log('[ERROR] Task Server -> Event Server Client -> setuptWebSockets -> sendCommand -> WebSocket message could not be sent because the connection was not ready. Message = ' + JSON.stringify(command))
|
||||
}
|
||||
} catch(err) {
|
||||
console.log('[ERROR] Task Server -> Event Server Client -> sendCommand -> err = ' + err.stack)
|
||||
}
|
||||
}
|
||||
|
||||
function createEventHandler(eventHandlerName, callerId, responseCallBack) {
|
||||
let eventCommand = {
|
||||
action: 'createEventHandler',
|
||||
eventHandlerName: eventHandlerName,
|
||||
callerId: callerId
|
||||
}
|
||||
sendCommand(eventCommand, responseCallBack)
|
||||
}
|
||||
|
||||
function deleteEventHandler(eventHandlerName, callerId, responseCallBack) {
|
||||
let eventCommand = {
|
||||
action: 'deleteEventHandler',
|
||||
eventHandlerName: eventHandlerName,
|
||||
callerId: callerId
|
||||
}
|
||||
sendCommand(eventCommand, responseCallBack)
|
||||
}
|
||||
|
||||
function listenToEvent(eventHandlerName, eventType, extraData, callerId, responseCallBack, eventsCallBack) {
|
||||
let eventCommand = {
|
||||
action: 'listenToEvent',
|
||||
eventHandlerName: eventHandlerName,
|
||||
eventType: eventType,
|
||||
extraData: extraData,
|
||||
callerId: callerId
|
||||
}
|
||||
sendCommand(eventCommand, responseCallBack, eventsCallBack)
|
||||
}
|
||||
|
||||
function stopListening(eventHandlerName, eventType, eventSubscriptionId, callerId, responseCallBack) {
|
||||
/* User either needs to specify a valid eventSubscriptionId OR the 3 params: eventHandlerName, eventType, callerId which were used when start listening to events. */
|
||||
let eventCommand = {
|
||||
action: 'stopListening',
|
||||
eventHandlerName: eventHandlerName,
|
||||
eventType: eventType,
|
||||
eventSubscriptionId: eventSubscriptionId,
|
||||
callerId: callerId
|
||||
}
|
||||
sendCommand(eventCommand, responseCallBack)
|
||||
}
|
||||
|
||||
function raiseEvent(eventHandlerName, eventType, event, callerId, responseCallBack) {
|
||||
let eventCommand = {
|
||||
action: 'raiseEvent',
|
||||
eventHandlerName: eventHandlerName,
|
||||
eventType: eventType,
|
||||
event: event,
|
||||
callerId: callerId
|
||||
}
|
||||
sendCommand(eventCommand, responseCallBack)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,532 @@
|
||||
|
||||
const path = require('path')
|
||||
|
||||
exports.newFileStorage = function newFileStorage(logger, host, port) {
|
||||
|
||||
const MODULE_NAME = 'FileStorage'
|
||||
const MAX_RETRY = 10
|
||||
const FAST_RETRY_TIME_IN_MILISECONDS = 500
|
||||
const SLOW_RETRY_TIME_IN_MILISECONDS = 2000
|
||||
|
||||
if (logger === undefined) { // Dummy logger
|
||||
logger = {}
|
||||
function write() {
|
||||
|
||||
}
|
||||
logger.write = write
|
||||
}
|
||||
|
||||
let thisObject = {
|
||||
asyncGetTextFile: asyncGetTextFile,
|
||||
asyncCreateTextFile: asyncCreateTextFile,
|
||||
getTextFile: getTextFile,
|
||||
createTextFile: createTextFile,
|
||||
deleteTextFile: deleteTextFile
|
||||
}
|
||||
|
||||
return thisObject
|
||||
|
||||
async function asyncGetTextFile(filePath, noRetry, canUsePrevious) {
|
||||
/* This function allows its caller to work with async / await instead of callbacks */
|
||||
let promise = new Promise((resolve, reject) => {
|
||||
|
||||
getTextFile(filePath, onFileRead, noRetry, canUsePrevious)
|
||||
function onFileRead(err, text) {
|
||||
|
||||
let response = {
|
||||
err: err,
|
||||
text: text
|
||||
}
|
||||
resolve(response)
|
||||
}
|
||||
})
|
||||
|
||||
return promise
|
||||
}
|
||||
|
||||
async function asyncCreateTextFile(filePath, fileContent, keepPrevious, noTemp) {
|
||||
/* This function allows its caller to work with async / await instead of callbacks */
|
||||
let promise = new Promise((resolve, reject) => {
|
||||
|
||||
createTextFile(filePath, fileContent, onFileWriten, keepPrevious, noTemp)
|
||||
function onFileWriten(err) {
|
||||
|
||||
let response = {
|
||||
err: err
|
||||
}
|
||||
resolve(response)
|
||||
}
|
||||
})
|
||||
|
||||
return promise
|
||||
}
|
||||
|
||||
function getTextFile(filePath, callBackFunction, noRetry, canUsePrevious) {
|
||||
|
||||
let currentRetryGetTextFile = 0
|
||||
|
||||
recursiveGetTextFile(filePath, callBackFunction, noRetry, canUsePrevious)
|
||||
|
||||
function recursiveGetTextFile(filePath, callBackFunction, noRetry, canUsePrevious) {
|
||||
let fileDoesNotExist = false
|
||||
|
||||
/* Choose path for either bots or data */
|
||||
let fileLocation
|
||||
let mustBeJason
|
||||
if (filePath.indexOf("/bots/") > 0) {
|
||||
fileLocation = process.env.BOTS_PATH + '/' + filePath
|
||||
mustBeJason = false
|
||||
} else {
|
||||
fileLocation = process.env.STORAGE_PATH + '/' + filePath
|
||||
mustBeJason = true
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
logger.write(MODULE_NAME, '[INFO] FileStorage -> getTextFile -> fileLocation: ' + fileLocation)
|
||||
|
||||
/* Here we actually reaad the file. */
|
||||
if (host === undefined || host === 'localhost') {
|
||||
/* We read the file from the local file system. */
|
||||
const fs = require('fs')
|
||||
fs.readFile(fileLocation, onFileRead)
|
||||
} else {
|
||||
/* We read the file via a web server over http */
|
||||
getFileViaHTTP(fileLocation, onFileRead, callBackFunction)
|
||||
}
|
||||
|
||||
function onFileRead(err, text) {
|
||||
let retryTimeToUse = FAST_RETRY_TIME_IN_MILISECONDS
|
||||
if (currentRetryGetTextFile > MAX_RETRY - 2) {
|
||||
retryTimeToUse = SLOW_RETRY_TIME_IN_MILISECONDS
|
||||
}
|
||||
if (err) {
|
||||
|
||||
if (err.code === 'ENOENT') { // since files are deleted before being replaced, it can happen that it does not exist and after a retry it does.
|
||||
if (noRetry === true) {
|
||||
logger.write(MODULE_NAME, '[WARN] FileStorage -> getTextFile -> onFileRead -> File does not exist. Not Retrying. ')
|
||||
let customResponse = {
|
||||
result: global.CUSTOM_FAIL_RESPONSE.result,
|
||||
message: 'File does not exist.'
|
||||
}
|
||||
callBackFunction(customResponse)
|
||||
return
|
||||
}
|
||||
|
||||
fileDoesNotExist = true
|
||||
logger.write(MODULE_NAME, '[WARN] FileStorage -> getTextFile -> onFileRead -> File does not exist. Retrying. ')
|
||||
setTimeout(retry, retryTimeToUse)
|
||||
return
|
||||
}
|
||||
|
||||
logger.write(MODULE_NAME, '[WARN] FileStorage -> getTextFile -> onFileRead -> Error reading file -> err = ' + err.stack)
|
||||
logger.write(MODULE_NAME, '[WARN] FileStorage -> getTextFile -> onFileRead -> Error reading file -> filePath = ' + filePath)
|
||||
logger.write(MODULE_NAME, '[WARN] FileStorage -> getTextFile -> onFileRead -> Error reading file -> err = ' + err.stack)
|
||||
setTimeout(retry, retryTimeToUse)
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
It might happen that we try to read a file just at the moment it is being written by another process. It that case we might get
|
||||
an empty file. If we are not allowed to use the Previous version of the file in this case, then we will retry to read it again
|
||||
a little bit later.
|
||||
*/
|
||||
if (text.toString() === "" && canUsePrevious !== true) {
|
||||
logger.write(MODULE_NAME, '[WARN] FileStorage -> getTextFile -> onFileRead -> Read and Empty File -> filePath = ' + filePath)
|
||||
setTimeout(retry, retryTimeToUse)
|
||||
return
|
||||
}
|
||||
|
||||
if (mustBeJason === false) {
|
||||
/* In this case there is nothing else to check. */
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE, text.toString())
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
We are going to check if the file is a valir JSON object
|
||||
*/
|
||||
try {
|
||||
let jsonCheck = JSON.parse(text.toString())
|
||||
|
||||
/* The file was correctly read and could be parsed. */
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE, text.toString())
|
||||
return
|
||||
|
||||
} catch (err) {
|
||||
|
||||
if (canUsePrevious === true) {
|
||||
logger.write(MODULE_NAME, '[WARN] FileStorage -> getTextFile -> onFileRead -> Could read the file, but could not parse it as it is not a valid JSON. Will try to read the PREVIOUS version instead. -> file = ' + fileLocation)
|
||||
|
||||
if (host === undefined || host === 'localhost') {
|
||||
/* We read the file from the local file system. */
|
||||
const fs = require('fs')
|
||||
fs.readFile(fileLocation + '.Previous.json', onPreviousFileRead)
|
||||
} else {
|
||||
/* We read the file via a web server over http */
|
||||
getFileViaHTTP(fileLocation + '.Previous.json', onPreviousFileRead, callBackFunction)
|
||||
}
|
||||
|
||||
function onPreviousFileRead(err, text) {
|
||||
|
||||
if (err) {
|
||||
logger.write(MODULE_NAME, '[WARN] FileStorage -> getTextFile -> onFileRead -> canUsePrevious -> Could not read the Previous file either. Giving up.')
|
||||
logger.write(MODULE_NAME, '[WARN] FileStorage -> getTextFile -> onFileRead -> canUsePrevious -> err = ' + err.stack)
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE)
|
||||
return
|
||||
}
|
||||
|
||||
/* Returning the previous file */
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE, text.toString())
|
||||
return
|
||||
}
|
||||
} else {
|
||||
logger.write(MODULE_NAME, '[WARN] FileStorage -> getTextFile -> onFileRead -> Could read the file, but could not parse it as it is not a valid JSON.')
|
||||
logger.write(MODULE_NAME, '[WARN] FileStorage -> getTextFile -> onFileRead -> err = ' + err.stack)
|
||||
logger.write(MODULE_NAME, '[WARN] FileStorage -> getTextFile -> onFileRead -> text.toString() = ' + text.toString())
|
||||
setTimeout(retry, retryTimeToUse)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, '[WARN] FileStorage -> getTextFile -> Error reading file -> file = ' + fileLocation)
|
||||
logger.write(MODULE_NAME, '[WARN] FileStorage -> getTextFile -> Error reading file -> err = ' + err.stack)
|
||||
retry()
|
||||
}
|
||||
|
||||
function retry() {
|
||||
if (currentRetryGetTextFile < MAX_RETRY) {
|
||||
currentRetryGetTextFile++
|
||||
logger.write(MODULE_NAME, '[INFO] FileStorage -> getTextFile -> retry -> Will try to read the file again -> Retry #: ' + currentRetryGetTextFile)
|
||||
recursiveGetTextFile(filePath, callBackFunction, noRetry, canUsePrevious)
|
||||
} else {
|
||||
currentRetryGetTextFile = 0
|
||||
|
||||
if (fileDoesNotExist === true) {
|
||||
logger.write(MODULE_NAME, '[WARN] FileStorage -> getTextFile -> retry -> Max retries reached reading a file. File Not Found.')
|
||||
let customResponse = {
|
||||
result: global.CUSTOM_FAIL_RESPONSE.result,
|
||||
message: 'File does not exist.'
|
||||
}
|
||||
callBackFunction(customResponse)
|
||||
return
|
||||
} else {
|
||||
|
||||
logger.write(MODULE_NAME, '[ERROR] FileStorage -> getTextFile -> retry -> Max retries reached reading a file. Giving up.')
|
||||
logger.write(MODULE_NAME, '[ERROR] FileStorage -> getTextFile -> retry -> file = ' + fileLocation)
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createTextFile(filePath, fileContent, callBackFunction, keepPrevious, noTemp) {
|
||||
|
||||
let currentRetryWriteTextFile = 0
|
||||
|
||||
recursiveCreateTextFile(filePath, fileContent, callBackFunction, keepPrevious, noTemp)
|
||||
|
||||
function recursiveCreateTextFile(filePath, fileContent, callBackFunction, keepPrevious, noTemp) {
|
||||
/* Choose path for either logs or data */
|
||||
let fileLocation
|
||||
if (filePath.indexOf("/Logs/") > 0) {
|
||||
fileLocation = process.env.LOG_PATH + '/' + filePath
|
||||
} else {
|
||||
fileLocation = process.env.STORAGE_PATH + '/' + filePath
|
||||
}
|
||||
|
||||
try {
|
||||
logger.write(MODULE_NAME, '[INFO] FileStorage -> createTextFile -> fileLocation: ' + fileLocation)
|
||||
|
||||
/* If necesary a folder or folders are created before writing the file to disk. */
|
||||
let directoryPath = fileLocation.substring(0, fileLocation.lastIndexOf('/') + 1);
|
||||
mkDirByPathSync(directoryPath)
|
||||
|
||||
/*
|
||||
Here we write the file with a temporary name so as to avoid dirty read from other processes.
|
||||
Then we delete the original file, if exists, and finally we rename the temporary into the original name.
|
||||
*/
|
||||
const fs = require('fs')
|
||||
if (noTemp === true) {
|
||||
fs.writeFile(fileLocation, fileContent, onFileWriten)
|
||||
} else {
|
||||
fs.writeFile(fileLocation + '.tmp', fileContent, onFileWriten)
|
||||
}
|
||||
|
||||
function onFileWriten(err) {
|
||||
let retryTimeToUse = FAST_RETRY_TIME_IN_MILISECONDS
|
||||
if (currentRetryWriteTextFile > MAX_RETRY - 2) {
|
||||
retryTimeToUse = SLOW_RETRY_TIME_IN_MILISECONDS
|
||||
}
|
||||
if (err) {
|
||||
logger.write(MODULE_NAME, '[WARN] FileStorage -> createTextFile -> onFileWriten -> Error writing file -> file = ' + fileLocation + '.tmp')
|
||||
logger.write(MODULE_NAME, '[WARN] FileStorage -> createTextFile -> onFileWriten -> Error writing file -> err = ' + err.stack)
|
||||
setTimeout(retry, retryTimeToUse)
|
||||
} else {
|
||||
|
||||
if (noTemp === true) {
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE)
|
||||
return
|
||||
}
|
||||
if (keepPrevious === true) {
|
||||
/*
|
||||
In some cases, we are going to keep a copy of the previous version of the file being written. This will be usefull to recover from crashes
|
||||
when the file written gets corrupted for any reason.
|
||||
*/
|
||||
fs.unlink(fileLocation + '.Previous.json', onUnlinked)
|
||||
|
||||
function onUnlinked(err) {
|
||||
let code = ''
|
||||
if (err) {
|
||||
code = err.code
|
||||
}
|
||||
if (code !== '' && code !== 'ENOENT') {
|
||||
logger.write(MODULE_NAME, '[WARN] FileStorage -> createTextFile -> onFileWriten -> onUnlinked -> Error deleting file -> file = ' + fileLocation + '.Previous.json')
|
||||
logger.write(MODULE_NAME, '[WARN] FileStorage -> createTextFile -> onFileWriten -> onUnlinked -> Error deleting file -> err = ' + err.stack)
|
||||
setTimeout(retry, retryTimeToUse)
|
||||
return
|
||||
}
|
||||
|
||||
/* Rename de Original into Previous */
|
||||
fs.rename(fileLocation, fileLocation + '.Previous.json', onOriginalRenamed)
|
||||
|
||||
function onOriginalRenamed(err) {
|
||||
let code = ''
|
||||
if (err) {
|
||||
code = err.code
|
||||
}
|
||||
if (code !== '' && code !== 'ENOENT') { // Unless the file does not exists we do this...
|
||||
logger.write(MODULE_NAME, '[WARN] FileStorage -> createTextFile -> onFileWriten -> onUnlinked -> onOriginalRenamed -> Error renaming original file -> file = ' + fileLocation)
|
||||
logger.write(MODULE_NAME, '[WARN] FileStorage -> createTextFile -> onFileWriten -> onUnlinked -> onOriginalRenamed -> Error renaming original file -> err = ' + err.stack)
|
||||
setTimeout(retry, retryTimeToUse)
|
||||
return
|
||||
}
|
||||
|
||||
/* Rename de Temp into Original */
|
||||
fs.rename(fileLocation + '.tmp', fileLocation, onTempRenamed)
|
||||
|
||||
function onTempRenamed(err) {
|
||||
if (err) {
|
||||
logger.write(MODULE_NAME, '[WARN] FileStorage -> createTextFile -> onFileWriten -> onUnlinked -> onOriginalRenamed -> onTempRenamed -> Error renaming temp file -> file = ' + fileLocation + '.tmp')
|
||||
logger.write(MODULE_NAME, '[WARN] FileStorage -> createTextFile -> onFileWriten -> onUnlinked -> onOriginalRenamed -> onTempRenamed -> Error renaming temp file -> err = ' + err.stack)
|
||||
setTimeout(retry, retryTimeToUse)
|
||||
} else {
|
||||
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
/* In this case, there is no need to keep a copy of the file being replaced, so we just delete and that's it. */
|
||||
fs.unlink(fileLocation, onUnlinked)
|
||||
|
||||
function onUnlinked(err) {
|
||||
let code = ''
|
||||
if (err) {
|
||||
code = err.code
|
||||
}
|
||||
if (code !== '' && code !== 'ENOENT') {
|
||||
logger.write(MODULE_NAME, '[WARN] FileStorage -> createTextFile -> onFileWriten -> onUnlinked -> Error deleting file -> file = ' + fileLocation)
|
||||
logger.write(MODULE_NAME, '[WARN] FileStorage -> createTextFile -> onFileWriten -> onUnlinked -> Error deleting file -> err = ' + err.stack)
|
||||
setTimeout(retry, retryTimeToUse)
|
||||
return
|
||||
}
|
||||
|
||||
/* Rename de Temp into Original */
|
||||
fs.rename(fileLocation + '.tmp', fileLocation, onTempRenamed)
|
||||
|
||||
function onTempRenamed(err) {
|
||||
if (err) {
|
||||
logger.write(MODULE_NAME, '[WARN] FileStorage -> createTextFile -> onFileWriten -> onUnlinked -> onOriginalRenamed -> onTempRenamed -> Error renaming temp file -> file = ' + fileLocation + '.tmp')
|
||||
logger.write(MODULE_NAME, '[WARN] FileStorage -> createTextFile -> onFileWriten -> onUnlinked -> onOriginalRenamed -> onTempRenamed -> Error renaming temp file -> err = ' + err.stack)
|
||||
setTimeout(retry, retryTimeToUse)
|
||||
} else {
|
||||
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, '[WARN] FileStorage -> createTextFile -> Error writing file -> file = ' + fileLocation)
|
||||
logger.write(MODULE_NAME, '[WARN] FileStorage -> createTextFile -> Error writing file -> err = ' + err.stack)
|
||||
retry()
|
||||
}
|
||||
|
||||
function retry() {
|
||||
if (currentRetryWriteTextFile < MAX_RETRY) {
|
||||
currentRetryWriteTextFile++
|
||||
logger.write(MODULE_NAME, '[INFO] FileStorage -> createTextFile -> retry -> Will try to write the file again -> Retry #: ' + currentRetryWriteTextFile)
|
||||
recursiveCreateTextFile(filePath, fileContent, callBackFunction, keepPrevious)
|
||||
} else {
|
||||
currentRetryWriteTextFile = 0
|
||||
logger.write(MODULE_NAME, '[ERROR] FileStorage -> createTextFile -> retry -> Max retries reached writting a file. Giving up.')
|
||||
logger.write(MODULE_NAME, '[ERROR] FileStorage -> createTextFile -> retry -> file = ' + fileLocation)
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function deleteTextFile(filePath, callBackFunction) {
|
||||
|
||||
let currentRetryDeleteTextFile = 0
|
||||
|
||||
recursiveDeleteTextFile(filePath, callBackFunction)
|
||||
|
||||
function recursiveDeleteTextFile(filePath, callBackFunction) {
|
||||
logger.write(MODULE_NAME, '[INFO] FileStorage -> deleteTextFile -> Entering Function.')
|
||||
|
||||
/* Choose path for either logs or data */
|
||||
let fileLocation
|
||||
if (filePath.indexOf("/Logs/") > 0) {
|
||||
fileLocation = process.env.LOG_PATH + '/' + filePath
|
||||
} else {
|
||||
fileLocation = process.env.STORAGE_PATH + '/' + filePath
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
logger.write(MODULE_NAME, '[INFO] FileStorage -> deleteTextFile -> fileLocation: ' + fileLocation)
|
||||
|
||||
const fs = require('fs')
|
||||
fs.unlink(fileLocation, onUnlinked)
|
||||
|
||||
function onUnlinked(err) {
|
||||
let retryTimeToUse = FAST_RETRY_TIME_IN_MILISECONDS
|
||||
if (currentRetryDeleteTextFile > MAX_RETRY - 2) {
|
||||
retryTimeToUse = SLOW_RETRY_TIME_IN_MILISECONDS
|
||||
}
|
||||
if (err) {
|
||||
if (callBackFunction !== undefined) {
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE)
|
||||
}
|
||||
} else {
|
||||
setTimeout(retry, retryTimeToUse)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, '[WARN] FileStorage -> deleteTextFile -> Error writing file -> file = ' + fileLocation)
|
||||
logger.write(MODULE_NAME, '[WARN] FileStorage -> deleteTextFile -> Error writing file -> err = ' + err.stack)
|
||||
if (callBackFunction !== undefined) {
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE)
|
||||
}
|
||||
}
|
||||
|
||||
function retry() {
|
||||
if (currentRetryDeleteTextFile < MAX_RETRY) {
|
||||
currentRetryDeleteTextFile++
|
||||
logger.write(MODULE_NAME, '[INFO] FileStorage -> deleteTextFile -> retry -> Will try to delete the file again -> Retry #: ' + currentRetryDeleteTextFile)
|
||||
recursiveDeleteTextFile(filePath, callBackFunction)
|
||||
} else {
|
||||
currentRetryDeleteTextFile = 0
|
||||
logger.write(MODULE_NAME, '[ERROR] FileStorage -> deleteTextFile -> retry -> Max retries reached deleting a file. Giving up.')
|
||||
logger.write(MODULE_NAME, '[ERROR] FileStorage -> deleteTextFile -> retry -> file = ' + fileLocation)
|
||||
if (callBackFunction !== undefined) {
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Function to create folders of missing folders at any path. */
|
||||
function mkDirByPathSync(targetDir, { isRelativeToScript = false } = {}) {
|
||||
const sep = '/';
|
||||
const initDir = path.isAbsolute(targetDir) ? sep : '';
|
||||
const baseDir = isRelativeToScript ? __dirname : '.';
|
||||
|
||||
return targetDir.split(sep).reduce((parentDir, childDir) => {
|
||||
const curDir = path.resolve(baseDir, parentDir, childDir);
|
||||
try {
|
||||
const fs = require('fs')
|
||||
fs.mkdirSync(curDir);
|
||||
} catch (err) {
|
||||
if (err.code === 'EEXIST') { // curDir already exists!
|
||||
return curDir;
|
||||
}
|
||||
|
||||
// To avoid `EISDIR` error on Mac and `EACCES`-->`ENOENT` and `EPERM` on Windows.
|
||||
if (err.code === 'ENOENT') { // Throw the original parentDir error on curDir `ENOENT` failure.
|
||||
throw new Error(`EACCES: permission denied, mkdir '${parentDir}'`);
|
||||
}
|
||||
|
||||
const caughtErr = ['EACCES', 'EPERM', 'EISDIR'].indexOf(err.code) > -1;
|
||||
if (!caughtErr || caughtErr && curDir === path.resolve(targetDir)) {
|
||||
throw err; // Throw if it's just the last created dir.
|
||||
}
|
||||
}
|
||||
|
||||
return curDir;
|
||||
}, initDir);
|
||||
}
|
||||
|
||||
function getFileViaHTTP(filePath, callback, callBackFunction) {
|
||||
try {
|
||||
|
||||
/* The filePath received is the one that is needed to fetch data from with fs. To do it via http we need to remove the prefix that includes this: ./Data-Storage/ */
|
||||
filePath = filePath.substring(15, filePath.length)
|
||||
|
||||
let http = require('http');
|
||||
let url = 'http://' + host +
|
||||
':' + port +
|
||||
'/Storage/' +
|
||||
filePath
|
||||
|
||||
logger.write(MODULE_NAME, '[INFO] FileStorage -> getFileViaHTTP -> url = ' + url)
|
||||
|
||||
let request = http.get(url, onResponse);
|
||||
|
||||
request.on('error', function (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] getFileViaHTTP -> onError -> err = " + err.stack);
|
||||
logger.write(MODULE_NAME, "[ERROR] getFileViaHTTP -> onError -> Failed to fetch file via HTTP. Will retry later. ");
|
||||
callBackFunction(global.DEFAULT_RETRY_RESPONSE);
|
||||
})
|
||||
|
||||
function onResponse(response) {
|
||||
|
||||
try {
|
||||
const chunks = []
|
||||
|
||||
response.on('data', onMessegesArrived)
|
||||
response.on('end', onEnd)
|
||||
|
||||
function onMessegesArrived(chunk) {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
|
||||
function onEnd() {
|
||||
let fileContent = Buffer.concat(chunks).toString('utf8')
|
||||
let err = null
|
||||
if (fileContent.indexOf('does not exist') >= 0) {
|
||||
err = {
|
||||
code: "ENOENT" // This is how fs would have returned upon this situation.
|
||||
}
|
||||
fileContent = undefined
|
||||
}
|
||||
callback(err, fileContent)
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] getFileViaHTTP -> onResponse -> err = " + err.stack);
|
||||
logger.write(MODULE_NAME, "[ERROR] getFileViaHTTP -> onResponse -> Failed to fetch file via HTTP. Will retry later. ");
|
||||
callBackFunction(global.DEFAULT_RETRY_RESPONSE);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] getFileViaHTTP -> err = " + err.stack);
|
||||
logger.write(MODULE_NAME, "[ERROR] getFileViaHTTP -> Failed to fetch file via HTTP. Will retry later. ");
|
||||
callBackFunction(global.DEFAULT_RETRY_RESPONSE);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,786 @@
|
||||
exports.newIndicatorBot = function newIndicatorBot(bot, parentLogger) {
|
||||
|
||||
const MODULE_NAME = "Indicator Bot";
|
||||
const FULL_LOG = true;
|
||||
|
||||
let USER_BOT_MODULE;
|
||||
let COMMONS_MODULE;
|
||||
|
||||
const MULTI_PERIOD_MARKET = require(global.ROOT_DIR + 'MultiPeriodMarket');
|
||||
const MULTI_PERIOD_DAILY = require(global.ROOT_DIR + 'MultiPeriodDaily');
|
||||
const FILE_STORAGE = require('./FileStorage.js');
|
||||
let fileStorage = FILE_STORAGE.newFileStorage(parentLogger);
|
||||
|
||||
const DEBUG_MODULE = require(global.ROOT_DIR + 'DebugLog');
|
||||
let logger; // We need this here in order for the loopHealth function to work and be able to rescue the loop when it gets in trouble.
|
||||
|
||||
let nextLoopTimeoutHandle;
|
||||
|
||||
let thisObject = {
|
||||
initialize: initialize,
|
||||
run: run
|
||||
};
|
||||
|
||||
let processConfig;
|
||||
|
||||
return thisObject;
|
||||
|
||||
function initialize(pProcessConfig, callBackFunction) {
|
||||
try {
|
||||
processConfig = pProcessConfig;
|
||||
|
||||
if (bot.definedByUI === true) {
|
||||
/* The code of the bot is defined at the UI. No need to load a file with the code. */
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE);
|
||||
return
|
||||
}
|
||||
|
||||
/* This bot is not ready for taking its code from the UI, then we need to load it from its repo. */
|
||||
let filePath = bot.dataMine + "/" + "bots" + "/" + bot.processNode.referenceParent.parentNode.config.repo + "/" + pProcessConfig.codeName
|
||||
filePath += "/User.Bot.js"
|
||||
|
||||
fileStorage.getTextFile(filePath, onBotDownloaded);
|
||||
|
||||
function onBotDownloaded(err, text) {
|
||||
|
||||
if (err.result !== global.DEFAULT_OK_RESPONSE.result) {
|
||||
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] initialize -> onInizialized -> onBotDownloaded -> err = " + err.message);
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] initialize -> onInizialized -> onBotDownloaded -> filePath = " + filePath);
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
return;
|
||||
}
|
||||
|
||||
USER_BOT_MODULE = {}
|
||||
USER_BOT_MODULE.newUserBot = eval(text); // TODO This needs to be changed function
|
||||
|
||||
filePath = bot.dataMine + "/" + "bots" + "/" + bot.processNode.referenceParent.parentNode.config.repo
|
||||
filePath += "/Commons.js"
|
||||
|
||||
fileStorage.getTextFile(filePath, onCommonsDownloaded);
|
||||
|
||||
function onCommonsDownloaded(err, text) {
|
||||
if (err.result !== global.DEFAULT_OK_RESPONSE.result) {
|
||||
parentLogger.write(MODULE_NAME, "[WARN] initialize -> onBotDownloaded -> onCommonsDownloaded -> Commons not found: " + err.message);
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE);
|
||||
return;
|
||||
}
|
||||
COMMONS_MODULE = {}
|
||||
COMMONS_MODULE.newCommons = eval(text); // TODO This needs to be changed function
|
||||
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] initialize -> err = " + err.stack);
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
}
|
||||
}
|
||||
|
||||
function run(callBackFunction) {
|
||||
try {
|
||||
let fixedTimeLoopIntervalHandle;
|
||||
|
||||
/* Heartbeats sent to the UI */
|
||||
bot.processHeartBeat = processHeartBeat
|
||||
|
||||
if (bot.runAtFixedInterval === true) {
|
||||
fixedTimeLoopIntervalHandle = setInterval(loop, bot.fixedInterval);
|
||||
} else {
|
||||
loop();
|
||||
}
|
||||
|
||||
function loop() {
|
||||
try {
|
||||
function pad(str, max) {
|
||||
str = str.toString();
|
||||
return str.length < max ? pad(" " + str, max) : str;
|
||||
}
|
||||
|
||||
/* For each loop we want to create a new log file. */
|
||||
if (logger !== undefined) {
|
||||
logger.finalize()
|
||||
}
|
||||
logger = DEBUG_MODULE.newDebugLog();
|
||||
global.LOGGER_MAP.set(MODULE_NAME, logger)
|
||||
logger.bot = bot;
|
||||
logger.initialize();
|
||||
|
||||
bot.loopCounter++;
|
||||
bot.loopStartTime = new Date().valueOf();
|
||||
|
||||
/* We tell the UI that we are running. */
|
||||
processHeartBeat()
|
||||
|
||||
/* We define here all the modules that the rest of the infraestructure, including the bots themselves can consume. */
|
||||
const UTILITIES = require(global.ROOT_DIR + 'CloudUtilities');
|
||||
const STATUS_REPORT = require(global.ROOT_DIR + 'StatusReport');
|
||||
const STATUS_DEPENDENCIES = require(global.ROOT_DIR + 'StatusDependencies');
|
||||
const DATA_DEPENDENCIES = require(global.ROOT_DIR + 'DataDependencies');
|
||||
const DATA_SET = require(global.ROOT_DIR + 'DataSet');
|
||||
const PROCESS_EXECUTION_EVENTS = require(global.ROOT_DIR + 'ProcessExecutionEvents');
|
||||
const PROCESS_OUTPUT = require(global.ROOT_DIR + 'ProcessOutput');
|
||||
|
||||
/* We define the datetime for the process that we are running now. This will be the official processing time for both the infraestructure and the bot. */
|
||||
bot.processDatetime = new Date(); // This will be considered the process date and time, so as to have it consistenly all over the execution.
|
||||
|
||||
/* High level log entry */
|
||||
console.log(new Date().toISOString() + " " + pad(bot.exchange, 20) + " " + pad(bot.market.baseAsset + '/' + bot.market.quotedAsset, 10) + " " + pad(bot.codeName, 30) + " " + pad(bot.process, 30)
|
||||
+ " Main Loop # " + pad(Number(bot.loopCounter), 8))
|
||||
|
||||
/* We will prepare first the infraestructure needed for the bot to run. There are 3 modules we need to sucessfullly initialize first. */
|
||||
let processExecutionEvents
|
||||
let userBot;
|
||||
let processFramework;
|
||||
let statusDependencies;
|
||||
let dataDependencies;
|
||||
|
||||
let nextWaitTime;
|
||||
|
||||
initializeProcessExecutionEvents();
|
||||
|
||||
function initializeProcessExecutionEvents() {
|
||||
try {
|
||||
processExecutionEvents = PROCESS_EXECUTION_EVENTS.newProcessExecutionEvents(bot, logger)
|
||||
processExecutionEvents.initialize(processConfig, onInizialized);
|
||||
|
||||
function onInizialized(err) {
|
||||
try {
|
||||
switch (err.result) {
|
||||
case global.DEFAULT_OK_RESPONSE.result: {
|
||||
logger.write(MODULE_NAME, "[INFO] run -> loop -> initializeProcessExecutionEvents -> onInizialized -> Execution finished well.");
|
||||
startProcessExecutionEvents()
|
||||
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, "[WARN] run -> loop -> initializeProcessExecutionEvents -> onInizialized -> Retry Later. Requesting Execution Retry.");
|
||||
nextWaitTime = 'Retry';
|
||||
loopControl(nextWaitTime);
|
||||
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] run -> loop -> initializeProcessExecutionEvents -> onInizialized -> Operation Failed. Aborting the process.");
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
default: {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeProcessExecutionEvents -> onInizialized -> Unhandled err.result received. -> err.result = " + err.result);
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeProcessExecutionEvents -> onInizialized -> Unhandled err.result received. -> err = " + err.message);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeProcessExecutionEvents -> onInizialized -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeProcessExecutionEvents -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function startProcessExecutionEvents() {
|
||||
try {
|
||||
processExecutionEvents.start(onStarted);
|
||||
|
||||
function onStarted(err) {
|
||||
try {
|
||||
switch (err.result) {
|
||||
case global.DEFAULT_OK_RESPONSE.result: {
|
||||
logger.write(MODULE_NAME, "[INFO] run -> loop -> startProcessExecutionEvents -> onStarted -> Execution finished well.");
|
||||
|
||||
if (global.STOP_TASK_GRACEFULLY === true) {
|
||||
loopControl()
|
||||
return
|
||||
}
|
||||
|
||||
initializeStatusDependencies();
|
||||
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, "[WARN] run -> loop -> startProcessExecutionEvents -> onStarted -> Retry Later. Requesting Execution Retry.");
|
||||
nextWaitTime = 'Retry';
|
||||
loopControl(nextWaitTime);
|
||||
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] run -> loop -> startProcessExecutionEvents -> onStarted -> Operation Failed. Aborting the process.");
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
default: {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> startProcessExecutionEvents -> onStarted -> Unhandled err.result received. -> err.result = " + err.result);
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> startProcessExecutionEvents -> onStarted -> Unhandled err.result received. -> err = " + err.message);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> startProcessExecutionEvents -> onStarted -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> startProcessExecutionEvents -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function initializeStatusDependencies() {
|
||||
try {
|
||||
statusDependencies = STATUS_DEPENDENCIES.newStatusDependencies(bot, logger, STATUS_REPORT, UTILITIES, PROCESS_OUTPUT);
|
||||
statusDependencies.initialize(onInizialized);
|
||||
|
||||
function onInizialized(err) {
|
||||
try {
|
||||
switch (err.result) {
|
||||
case global.DEFAULT_OK_RESPONSE.result: {
|
||||
logger.write(MODULE_NAME, "[INFO] run -> loop -> initializeStatusDependencies -> onInizialized -> Execution finished well.");
|
||||
initializeDataDependencies();
|
||||
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, "[WARN] run -> loop -> initializeStatusDependencies -> onInizialized -> Retry Later. Requesting Execution Retry.");
|
||||
nextWaitTime = 'Retry';
|
||||
loopControl(nextWaitTime);
|
||||
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] run -> loop -> initializeStatusDependencies -> onInizialized -> Operation Failed. Aborting the process.");
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
default: {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeStatusDependencies -> onInizialized -> Unhandled err.result received. -> err.result = " + err.result);
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeStatusDependencies -> onInizialized -> Unhandled err.result received. -> err = " + err.message);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeStatusDependencies -> onInizialized -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeStatusDependencies -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function initializeDataDependencies() {
|
||||
try {
|
||||
dataDependencies = DATA_DEPENDENCIES.newDataDependencies(bot, logger, DATA_SET);
|
||||
dataDependencies.initialize(onInizialized);
|
||||
|
||||
function onInizialized(err) {
|
||||
try {
|
||||
switch (err.result) {
|
||||
case global.DEFAULT_OK_RESPONSE.result: {
|
||||
/* If the process is configured to run inside a framework, we continue there, otherwise we run the bot directly. */
|
||||
if (processConfig.framework === undefined) {
|
||||
initializeUserBot();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (processConfig.framework.name) {
|
||||
case 'Multi-Period-Market': {
|
||||
processFramework = MULTI_PERIOD_MARKET.newMultiPeriodMarket(bot, logger, UTILITIES, FILE_STORAGE);
|
||||
intitializeProcessFramework();
|
||||
break;
|
||||
}
|
||||
case 'Multi-Period-Daily': {
|
||||
processFramework = MULTI_PERIOD_DAILY.newMultiPeriodDaily(bot, logger, UTILITIES, FILE_STORAGE);
|
||||
intitializeProcessFramework();
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeDataDependencies -> onInizialized -> Process Framework not Supported.");
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeDataDependencies -> onInizialized -> Process Framework Name = " + processConfig.framework.name);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
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, "[WARN] run -> loop -> initializeDataDependencies -> onInizialized -> Retry Later. Requesting Execution Retry.");
|
||||
nextWaitTime = 'Retry';
|
||||
loopControl(nextWaitTime);
|
||||
return;
|
||||
}
|
||||
case global.DEFAULT_FAIL_RESPONSE.result: { // This is an unexpected exception that we do not know how to handle.
|
||||
processStopped()
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeDataDependencies -> onInizialized -> Operation Failed. Aborting the process.");
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
default: {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeDataDependencies -> onInizialized -> Unhandled err.result received. -> err.result = " + err.result);
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeDataDependencies -> onInizialized -> Unhandled err.result received. -> err = " + err.message);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeDataDependencies -> onInizialized -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeDataDependencies -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function initializeUserBot() {
|
||||
try {
|
||||
usertBot = USER_BOT_MODULE.newUserBot(bot, logger, COMMONS_MODULE, UTILITIES, FILE_STORAGE);
|
||||
usertBot.initialize(statusDependencies, onInizialized);
|
||||
|
||||
function onInizialized(err) {
|
||||
try {
|
||||
switch (err.result) {
|
||||
case global.DEFAULT_OK_RESPONSE.result: {
|
||||
logger.write(MODULE_NAME, "[INFO] run -> loop -> initializeUserBot -> onInizialized -> Execution finished well.");
|
||||
startUserBot();
|
||||
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, "[WARN] run -> loop -> initializeUserBot -> onInizialized -> Retry Later. Requesting Execution Retry.");
|
||||
nextWaitTime = 'Retry';
|
||||
loopControl(nextWaitTime);
|
||||
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] run -> loop -> initializeUserBot -> onInizialized -> Operation Failed. Aborting the process.");
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
case global.CUSTOM_OK_RESPONSE.result: {
|
||||
|
||||
switch (err.message) {
|
||||
default: {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeUserBot -> onInizialized > Unhandled custom response received. -> err = " + err.message);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
default: {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeUserBot -> onInizialized -> Unhandled err.result received. -> err.result = " + err.result);
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeUserBot -> onInizialized -> Unhandled err.result received. -> err = " + err.message);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeUserBot -> onInizialized -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeUserBot -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function startUserBot() {
|
||||
try {
|
||||
usertBot.start(onFinished);
|
||||
|
||||
function onFinished(err) {
|
||||
try {
|
||||
switch (err.result) {
|
||||
case global.DEFAULT_OK_RESPONSE.result: {
|
||||
logger.write(MODULE_NAME, "[INFO] run -> loop -> startUserBot -> onFinished -> Execution finished well.");
|
||||
finishProcessExecutionEvents()
|
||||
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, "[WARN] run -> loop -> startUserBot -> onFinished -> Retry Later. Requesting Execution Retry.");
|
||||
nextWaitTime = 'Retry';
|
||||
loopControl(nextWaitTime);
|
||||
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] run -> loop -> startUserBot -> onFinished -> Operation Failed. Aborting the process.");
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
case global.CUSTOM_OK_RESPONSE.result: {
|
||||
|
||||
switch (err.message) {
|
||||
case "Dependency does not exist.": {
|
||||
logger.write(MODULE_NAME, "[WARN] run -> loop -> startUserBot -> onFinished -> Dependency does not exist. This Loop will go to sleep.");
|
||||
nextWaitTime = 'Sleep';
|
||||
loopControl(nextWaitTime);
|
||||
return;
|
||||
}
|
||||
case "Dependency not ready.": {
|
||||
logger.write(MODULE_NAME, "[WARN] run -> loop -> startUserBot -> onFinished -> Dependency not ready. Will Retry Later.");
|
||||
nextWaitTime = 'Retry';
|
||||
loopControl(nextWaitTime);
|
||||
return;
|
||||
}
|
||||
default: {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> startUserBot -> onFinished -> Unhandled custom response received. -> err = " + err.message);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
default: {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> startUserBot -> onFinished -> Unhandled err.result received. -> err.result = " + err.result);
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> startUserBot -> onFinished -> Unhandled err.result received. -> err = " + err.message);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> startUserBot -> onFinished -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> startUserBot -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function intitializeProcessFramework() {
|
||||
try {
|
||||
processFramework.initialize(processConfig, statusDependencies, dataDependencies, onInizialized);
|
||||
|
||||
function onInizialized(err) {
|
||||
try {
|
||||
switch (err.result) {
|
||||
case global.DEFAULT_OK_RESPONSE.result: {
|
||||
logger.write(MODULE_NAME, "[INFO] run -> loop -> intitializeProcessFramework -> onInizialized -> Execution finished well.");
|
||||
startProcessFramework();
|
||||
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, "[WARN] run -> loop -> intitializeProcessFramework -> onInizialized -> Retry Later. Requesting Execution Retry.");
|
||||
nextWaitTime = 'Retry';
|
||||
loopControl(nextWaitTime);
|
||||
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] run -> loop -> intitializeProcessFramework -> onInizialized -> Operation Failed. Aborting the process.");
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
case global.CUSTOM_OK_RESPONSE.result: {
|
||||
|
||||
switch (err.message) {
|
||||
default: {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> intitializeProcessFramework -> onInizialized > Unhandled custom response received. -> err = " + err.message);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
default: {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> intitializeProcessFramework -> onInizialized -> Unhandled err.result received. -> err.result = " + err.result);
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> intitializeProcessFramework -> onInizialized -> Unhandled err.result received. -> err = " + err.message);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> intitializeProcessFramework -> onInizialized -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> intitializeProcessFramework -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function startProcessFramework() {
|
||||
try {
|
||||
processFramework.start(onFinished);
|
||||
|
||||
function onFinished(err) {
|
||||
try {
|
||||
processFramework.finalize()
|
||||
processFramework = undefined
|
||||
dataDependencies.finalize()
|
||||
dataDependencies = undefined
|
||||
statusDependencies.finalize()
|
||||
statusDependencies = undefined
|
||||
|
||||
switch (err.result) {
|
||||
case global.DEFAULT_OK_RESPONSE.result: {
|
||||
logger.write(MODULE_NAME, "[INFO] run -> loop -> startProcessFramework -> onFinished -> Execution finished well.");
|
||||
finishProcessExecutionEvents()
|
||||
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, "[WARN] run -> loop -> startProcessFramework -> onFinished -> Retry Later. Requesting Execution Retry.");
|
||||
nextWaitTime = 'Retry';
|
||||
loopControl(nextWaitTime);
|
||||
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] run -> loop -> startProcessFramework -> onFinished -> Operation Failed. Aborting the process.");
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
case global.CUSTOM_OK_RESPONSE.result: {
|
||||
|
||||
switch (err.message) {
|
||||
case "Dependency does not exist.": {
|
||||
logger.write(MODULE_NAME, "[WARN] run -> loop -> startProcessFramework -> onFinished -> Dependency does not exist. This Loop will go to sleep.");
|
||||
nextWaitTime = 'Sleep';
|
||||
loopControl(nextWaitTime);
|
||||
return;
|
||||
}
|
||||
case "Dependency not ready.": {
|
||||
logger.write(MODULE_NAME, "[WARN] run -> loop -> startProcessFramework -> onFinished -> Dependency not ready. This Loop will go to sleep.");
|
||||
nextWaitTime = 'Sleep';
|
||||
loopControl(nextWaitTime);
|
||||
return;
|
||||
}
|
||||
default: {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> startProcessFramework -> onFinished -> Unhandled custom response received. -> err = " + err.message);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
default: {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> startProcessFramework -> onFinished -> Unhandled err.result received. -> err.result = " + err.result);
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> startProcessFramework -> onFinished -> Unhandled err.result received. -> err = " + err.message);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> startProcessFramework -> onFinished -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> startProcessFramework -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function finishProcessExecutionEvents() {
|
||||
try {
|
||||
processExecutionEvents.finish(onFinished);
|
||||
|
||||
function onFinished(err) {
|
||||
try {
|
||||
processExecutionEvents.finalize()
|
||||
processExecutionEvents = undefined
|
||||
|
||||
switch (err.result) {
|
||||
case global.DEFAULT_OK_RESPONSE.result: {
|
||||
logger.write(MODULE_NAME, "[INFO] run -> loop -> finishProcessExecutionEvents -> onFinished -> Execution finished well.");
|
||||
nextWaitTime = 'Normal';
|
||||
loopControl(nextWaitTime);
|
||||
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, "[WARN] run -> loop -> finishProcessExecutionEvents -> onFinished -> Retry Later. Requesting Execution Retry.");
|
||||
nextWaitTime = 'Retry';
|
||||
loopControl(nextWaitTime);
|
||||
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] run -> loop -> finishProcessExecutionEvents -> onFinished -> Operation Failed. Aborting the process.");
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
default: {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> finishProcessExecutionEvents -> onFinished -> Unhandled err.result received. -> err.result = " + err.result);
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> finishProcessExecutionEvents -> onFinished -> Unhandled err.result received. -> err = " + err.message);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> finishProcessExecutionEvents -> onFinished -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> finishProcessExecutionEvents -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function loopControl(nextWaitTime) {
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] run -> loop -> loopControl -> nextWaitTime = " + nextWaitTime); }
|
||||
|
||||
/* We show we reached the end of the loop. */
|
||||
processHeartBeat()
|
||||
|
||||
/* Here we check if we must stop the loop gracefully. */
|
||||
shallWeStop(onStop, onContinue);
|
||||
|
||||
function onStop() {
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] run -> loop -> loopControl -> onStop -> Stopping the Loop Gracefully. See you next time!"); }
|
||||
processStopped()
|
||||
return;
|
||||
}
|
||||
|
||||
function onContinue() {
|
||||
/* Indicator bots are going to be executed after a configured period of time after the last execution ended. This is to avoid overlapping executions. */
|
||||
switch (nextWaitTime) {
|
||||
case 'Normal': {
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] run -> loop -> loopControl -> Restarting Loop in " + (processConfig.normalWaitTime / 1000) + " seconds."); }
|
||||
nextLoopTimeoutHandle = setTimeout(loop, processConfig.normalWaitTime);
|
||||
processHeartBeat(undefined, undefined, "Waiting " + processConfig.normalWaitTime / 1000 + " seconds for next execution.")
|
||||
if (global.WRITE_LOGS_TO_FILES === 'true') {
|
||||
logger.persist();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'Retry': {
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] run -> loop -> loopControl -> Restarting Loop in " + (processConfig.retryWaitTime / 1000) + " seconds."); }
|
||||
nextLoopTimeoutHandle = setTimeout(loop, processConfig.retryWaitTime);
|
||||
processHeartBeat(undefined, undefined, "Trying to recover from some problem. Waiting " + processConfig.retryWaitTime / 1000 + " seconds for next execution.")
|
||||
logger.persist();
|
||||
}
|
||||
break;
|
||||
case 'Sleep': {
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] run -> loop -> loopControl -> Restarting Loop in " + (processConfig.sleepWaitTime / 60000) + " minutes."); }
|
||||
nextLoopTimeoutHandle = setTimeout(loop, processConfig.sleepWaitTime);
|
||||
processHeartBeat(undefined, undefined, "Waiting " + processConfig.sleepWaitTime / 60000 + " minutes for next execution.")
|
||||
logger.persist();
|
||||
}
|
||||
break;
|
||||
case 'Coma': {
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] run -> loop -> loopControl -> Restarting Loop in " + (processConfig.comaWaitTime / 3600000) + " hours."); }
|
||||
nextLoopTimeoutHandle = setTimeout(loop, processConfig.comaWaitTime);
|
||||
processHeartBeat(undefined, undefined, "Waiting " + processConfig.comaWaitTime / 3600000 + " hours for next execution.")
|
||||
logger.persist();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function shallWeStop(stopCallBack, continueCallBack) {
|
||||
try {
|
||||
/* IMPORTANT: This function is exactly the same on the 3 modules. */
|
||||
if (!global.STOP_TASK_GRACEFULLY) {
|
||||
continueCallBack();
|
||||
} else {
|
||||
stopCallBack();
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> shallWeStop -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] run -> loop -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function processHeartBeat(processingDate, percentage, status) {
|
||||
let event = {
|
||||
seconds: (new Date()).getSeconds(),
|
||||
processingDate: processingDate,
|
||||
percentage: percentage,
|
||||
status: status
|
||||
}
|
||||
global.EVENT_SERVER_CLIENT.raiseEvent(bot.processKey, 'Heartbeat', event)
|
||||
}
|
||||
|
||||
function processStopped() {
|
||||
if (global.unexpectedError !== undefined) {
|
||||
global.PROCESS_ERROR(bot.processKey, undefined, "An unexpected error caused the Process to stop.")
|
||||
} else {
|
||||
global.EVENT_SERVER_CLIENT.raiseEvent(bot.processKey, 'Stopped')
|
||||
}
|
||||
logger.persist();
|
||||
clearInterval(fixedTimeLoopIntervalHandle);
|
||||
clearTimeout(nextLoopTimeoutHandle);
|
||||
if (global.unexpectedError !== undefined) {
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE)
|
||||
} else {
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE)
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] run -> err = " + err.stack);
|
||||
clearInterval(fixedTimeLoopIntervalHandle);
|
||||
clearTimeout(nextLoopTimeoutHandle);
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,157 @@
|
||||
exports.newIndicatorOutput = function newIndicatorOutput(bot, logger, UTILITIES, FILE_STORAGE) {
|
||||
const FULL_LOG = true;
|
||||
const MODULE_NAME = "Indicator Bot";
|
||||
|
||||
let thisObject = {
|
||||
initialize: initialize,
|
||||
finalize: finalize,
|
||||
start: start
|
||||
};
|
||||
|
||||
let utilities = UTILITIES.newCloudUtilities(logger);
|
||||
let fileStorage = FILE_STORAGE.newFileStorage(logger);
|
||||
|
||||
const COMMONS = require('./Commons.js');
|
||||
let commons = COMMONS.newCommons(bot, logger, UTILITIES, FILE_STORAGE);
|
||||
|
||||
return thisObject;
|
||||
|
||||
function finalize() {
|
||||
thisObject = undefined
|
||||
utilities = undefined
|
||||
fileStorage = undefined
|
||||
commons = undefined
|
||||
}
|
||||
|
||||
function initialize(callBackFunction) {
|
||||
try {
|
||||
logger.fileName = MODULE_NAME;
|
||||
logger.initialize();
|
||||
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE);
|
||||
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] initialize -> err = " + err.stack);
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
}
|
||||
}
|
||||
|
||||
function start(dataFiles, timeFrame, timeFrameLabel, currentDay, interExecutionMemory, callBackFunction) {
|
||||
|
||||
try {
|
||||
|
||||
let products = {}
|
||||
let mainDependency = {}
|
||||
let processingDailyFiles
|
||||
|
||||
if (currentDay !== undefined) {
|
||||
processingDailyFiles = true
|
||||
} else {
|
||||
processingDailyFiles = false
|
||||
}
|
||||
|
||||
/* The first phase here is about checking that we have everything we need at the definition level. */
|
||||
let dataDependencies = global.NODE_BRANCH_TO_ARRAY(bot.processNode.referenceParent.processDependencies, 'Data Dependency')
|
||||
|
||||
if (commons.validateDataDependencies(dataDependencies, callBackFunction) !== true) { return }
|
||||
|
||||
let outputDatasets = global.NODE_BRANCH_TO_ARRAY (bot.processNode.referenceParent.processOutput, 'Output Dataset')
|
||||
if (commons.validateOutputDatasets(outputDatasets, callBackFunction) !== true) { return }
|
||||
|
||||
/* The second phase is about transforming the inputs into a format that can be used to apply the user defined code. */
|
||||
commons.inflateDatafiles(dataFiles, dataDependencies, products, mainDependency, timeFrame)
|
||||
|
||||
/* During the third phase, we need to generate the data of the different products this process produces an output */
|
||||
for (let i = 0; i < outputDatasets.length; i++) {
|
||||
|
||||
let outputDatasetNode = outputDatasets[i]
|
||||
let jsonData // Just build data as Json objects
|
||||
let outputData // Data built as a result of applying user defined code and formulas at the Data Building Procedure
|
||||
let singularVariableName // Name of the variable for this product
|
||||
let recordDefinition // Record as defined by the user at the UI
|
||||
let fileContent // Here we store the contents of the new data built just before writing it to a file.
|
||||
let resultsWithIrregularPeriods // A product will have irregular periods when the User Code inserts new result records at will, in contrast with normal procedure where the platform insert one record per loop execution.
|
||||
|
||||
/*
|
||||
For each outputDatasetNode in our process output, we will build the information based on our input products.
|
||||
*/
|
||||
|
||||
recordDefinition = outputDatasetNode.referenceParent.parentNode.record
|
||||
singularVariableName = outputDatasetNode.referenceParent.parentNode.config.singularVariableName
|
||||
|
||||
/* Check Irregular Periods */
|
||||
|
||||
if (outputDatasetNode.referenceParent.parentNode.dataBuilding.loop.javascriptCode !== undefined) {
|
||||
if (outputDatasetNode.referenceParent.parentNode.dataBuilding.loop.javascriptCode.code.indexOf('results.push') >= 0) {
|
||||
resultsWithIrregularPeriods = true
|
||||
}
|
||||
}
|
||||
|
||||
/* Build the data */
|
||||
jsonData = commons.dataBuildingProcedure(
|
||||
products,
|
||||
mainDependency,
|
||||
recordDefinition,
|
||||
outputDatasetNode.referenceParent.parentNode.dataBuilding,
|
||||
singularVariableName,
|
||||
outputDatasetNode.referenceParent.parentNode.config.codeName,
|
||||
timeFrame,
|
||||
timeFrameLabel,
|
||||
resultsWithIrregularPeriods,
|
||||
interExecutionMemory,
|
||||
processingDailyFiles,
|
||||
currentDay
|
||||
)
|
||||
|
||||
/* Add the calculated properties */
|
||||
if (outputDatasetNode.referenceParent.parentNode.calculations !== undefined) {
|
||||
outputData = commons.calculationsProcedure(jsonData, recordDefinition, outputDatasetNode.referenceParent.parentNode.calculations, singularVariableName, timeFrame)
|
||||
} else {
|
||||
outputData = jsonData
|
||||
}
|
||||
products[outputDatasetNode.referenceParent.parentNode.config.pluralVariableName] = outputData
|
||||
}
|
||||
|
||||
/*At the fourth and last phase, we will save the new information generated into files corresponding to each output outputDatasetNode.*/
|
||||
let totalFilesWritten = 0
|
||||
for (let i = 0; i < outputDatasets.length; i++) {
|
||||
let outputDatasetNode = outputDatasets[i]
|
||||
let outputData = products[outputDatasetNode.referenceParent.parentNode.config.pluralVariableName]
|
||||
let resultsWithIrregularPeriods // A product will have irregular periods when the User Code inserts new result records at will, in contrast with normal procedure where the platform insert one record per loop execution.
|
||||
let contextSummary = {}
|
||||
|
||||
/* Check Irregular Periods */
|
||||
if (outputDatasetNode.referenceParent.parentNode.dataBuilding.loop.javascriptCode.code.indexOf('results.push') >= 0) {
|
||||
resultsWithIrregularPeriods = true
|
||||
}
|
||||
|
||||
/* Simplifying the access to basic info */
|
||||
|
||||
contextSummary.dataset = outputDatasetNode.referenceParent.config.codeName
|
||||
contextSummary.product = outputDatasetNode.referenceParent.parentNode.config.codeName
|
||||
|
||||
let botNode = global.FIND_NODE_IN_NODE_MESH(outputDatasetNode, 'Indicator Bot')
|
||||
contextSummary.bot = botNode.config.codeName
|
||||
let dataMineNode = global.FIND_NODE_IN_NODE_MESH(outputDatasetNode, 'Data Mine')
|
||||
contextSummary.dataMine = dataMineNode.config.codeName
|
||||
contextSummary.mineType = dataMineNode.type.replace(' ', '-')
|
||||
contextSummary.project = dataMineNode.project
|
||||
|
||||
let fileContent = commons.generateFileContent(outputData, outputDatasetNode.referenceParent.parentNode.record, resultsWithIrregularPeriods, processingDailyFiles, currentDay, callBackFunction)
|
||||
commons.writeFile(contextSummary, fileContent, anotherFileWritten, processingDailyFiles, timeFrameLabel, currentDay, callBackFunction)
|
||||
}
|
||||
|
||||
|
||||
function anotherFileWritten() {
|
||||
totalFilesWritten++
|
||||
if (totalFilesWritten === outputDatasets.length) {
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> err = " + err.stack);
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,519 @@
|
||||
exports.newLearningSession = function newLearningSession(bot, parentLogger) {
|
||||
|
||||
const MODULE_NAME = "Learning Session"
|
||||
const FULL_LOG = true;
|
||||
|
||||
let thisObject = {
|
||||
initialize: initialize
|
||||
}
|
||||
|
||||
const SOCIAL_BOTS_MODULE = require('./SocialBots.js')
|
||||
let socialBotsModule = SOCIAL_BOTS_MODULE.newSocialBots(bot, parentLogger)
|
||||
|
||||
return thisObject;
|
||||
|
||||
function initialize(pProcessConfig, callBackFunction) {
|
||||
try {
|
||||
/* Initialize this info so that everything is logged propeerly */
|
||||
bot.LEARNING_SESSION = {
|
||||
name: bot.processNode.session.name,
|
||||
id: bot.processNode.session.id
|
||||
}
|
||||
|
||||
/* Set the folderName for early logging */
|
||||
if (bot.processNode.session.config.folderName === undefined) {
|
||||
bot.LEARNING_SESSION.folderName = bot.processNode.session.type.replace(' ', '-').replace(' ', '-') + '-' + bot.LEARNING_SESSION.id
|
||||
} else {
|
||||
bot.LEARNING_SESSION.folderName = bot.processNode.session.type.replace(' ', '-').replace(' ', '-') + '-' + bot.processNode.session.config.folderName
|
||||
}
|
||||
|
||||
/* Check if there is a session */
|
||||
if (bot.processNode.session === undefined) {
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] initialize -> Cannot run without a Session.");
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Listen to event to start or stop the session. */
|
||||
bot.sessionKey = bot.processNode.session.name + '-' + bot.processNode.session.type + '-' + bot.processNode.session.id
|
||||
global.SESSION_MAP.set(bot.sessionKey, bot.sessionKey)
|
||||
|
||||
global.EVENT_SERVER_CLIENT.listenToEvent(bot.sessionKey, 'Learning Session Status', undefined, bot.sessionKey, undefined, onSessionStatus)
|
||||
global.EVENT_SERVER_CLIENT.listenToEvent(bot.sessionKey, 'Run Learning Session', undefined, bot.sessionKey, undefined, onSessionRun)
|
||||
global.EVENT_SERVER_CLIENT.listenToEvent(bot.sessionKey, 'Stop Learning Session', undefined, bot.sessionKey, undefined, onSessionStop)
|
||||
global.EVENT_SERVER_CLIENT.listenToEvent(bot.sessionKey, 'Resume Learning Session', undefined, bot.sessionKey, undefined, onSessionResume)
|
||||
|
||||
/* Connect this here so that it is accesible from other places */
|
||||
bot.sessionError = sessionError
|
||||
bot.sessionWarning = sessionWarning
|
||||
bot.sessionInfo = sessionInfo
|
||||
|
||||
/* Heartbeats sent to the UI */
|
||||
bot.sessionHeartBeat = sessionHeartBeat
|
||||
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE)
|
||||
return
|
||||
|
||||
function onSessionStatus() {
|
||||
if (bot.SESSION_STATUS === 'Running') {
|
||||
let event = {
|
||||
status: 'Learning Session Runnning'
|
||||
}
|
||||
global.EVENT_SERVER_CLIENT.raiseEvent(bot.sessionKey, 'Status Response', event)
|
||||
} else {
|
||||
let event = {
|
||||
status: 'Learning Session Not Runnning'
|
||||
}
|
||||
global.EVENT_SERVER_CLIENT.raiseEvent(bot.sessionKey, 'Status Response', event)
|
||||
}
|
||||
}
|
||||
|
||||
function onSessionRun(message) {
|
||||
try {
|
||||
/* This happens when the UI is reloaded, the session was running and tries to run it again. */
|
||||
if (bot.SESSION_STATUS === 'Idle' || bot.SESSION_STATUS === 'Running') {
|
||||
parentLogger.write(MODULE_NAME, "[WARN] onSessionRun -> Event received to run the Session while it was already running. ")
|
||||
return
|
||||
}
|
||||
|
||||
/* We are going to run the Definition comming at the event. */
|
||||
bot.TRADING_SYSTEM = JSON.parse(message.event.tradingSystem)
|
||||
bot.TRADING_ENGINE = JSON.parse(message.event.tradingEngine)
|
||||
bot.LEARNING_SESSION = JSON.parse(message.event.session)
|
||||
bot.DEPENDENCY_FILTER = JSON.parse(message.event.dependencyFilter)
|
||||
bot.RESUME = false
|
||||
bot.FIRST_EXECUTION = true
|
||||
bot.LEARNING_SESSION.stop = stopSession // stop function
|
||||
|
||||
setUpSessionFolderName()
|
||||
|
||||
/* We validate all parameters received and complete some that might be missing if needed. */
|
||||
if (checkParemeters() === false) { return }
|
||||
|
||||
socialBotsModule.initialize()
|
||||
|
||||
let allGood
|
||||
switch (bot.LEARNING_SESSION.type) {
|
||||
case 'Backtesting Session': {
|
||||
allGood = startBackTesting(message)
|
||||
break
|
||||
}
|
||||
case 'Live Trading Session': {
|
||||
allGood = startLiveTrading(message)
|
||||
break
|
||||
}
|
||||
case 'Fordward Testing Session': {
|
||||
allGood = startFordwardTesting(message)
|
||||
break
|
||||
}
|
||||
case 'Paper Trading Session': {
|
||||
allGood = startPaperTrading(message)
|
||||
break
|
||||
}
|
||||
}
|
||||
if (allGood === true) {
|
||||
bot.SESSION_STATUS = 'Idle'
|
||||
bot.STOP_SESSION = false
|
||||
} else {
|
||||
bot.STOP_SESSION = true
|
||||
if (FULL_LOG === true) { parentLogger.write(MODULE_NAME, '[IMPORTANT] onSessionRun -> Stopping the Session now. ') }
|
||||
}
|
||||
|
||||
socialBotsModule.sendMessage(bot.LEARNING_SESSION.type + " '" + bot.LEARNING_SESSION.name + "' is starting.")
|
||||
} catch (err) {
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] initialize -> onSessionRun -> err = " + err.stack);
|
||||
}
|
||||
}
|
||||
|
||||
function onSessionStop() {
|
||||
stopSession('Session Stopped From the User Interface.')
|
||||
}
|
||||
|
||||
function onSessionResume(message) {
|
||||
try {
|
||||
if (bot.LEARNING_SESSION.stop === undefined) {
|
||||
parentLogger.write(MODULE_NAME, "[WARN] onSessionResume -> Event received to resume the Session that have never be ran before. ")
|
||||
return
|
||||
}
|
||||
|
||||
/* This happens when the UI is reloaded, the session was running and tries to run it again. */
|
||||
if (bot.SESSION_STATUS === 'Idle' || bot.SESSION_STATUS === 'Running') {
|
||||
parentLogger.write(MODULE_NAME, "[WARN] onSessionResume -> Event received to resume the Session while it was already running. ")
|
||||
return
|
||||
}
|
||||
|
||||
bot.RESUME = true
|
||||
bot.STOP_SESSION = false
|
||||
|
||||
socialBotsModule.sendMessage(bot.LEARNING_SESSION.type + " '" + bot.LEARNING_SESSION.name + "' is resuming.")
|
||||
} catch (err) {
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] initialize -> onSessionResume -> err = " + err.stack);
|
||||
}
|
||||
}
|
||||
|
||||
function stopSession(commandOrigin) {
|
||||
|
||||
socialBotsModule.sendMessage(bot.LEARNING_SESSION.type + " '" + bot.LEARNING_SESSION.name + "' is stopping " + commandOrigin)
|
||||
socialBotsModule.finalize()
|
||||
bot.STOP_SESSION = true
|
||||
if (FULL_LOG === true) { parentLogger.write(MODULE_NAME, '[IMPORTANT] stopSession -> Stopping the Session now. ') }
|
||||
sessionInfo(bot.LEARNING_SESSION, commandOrigin)
|
||||
}
|
||||
|
||||
function setUpSessionFolderName() {
|
||||
/*
|
||||
The session object is overwritten when the session is run. For that reason we
|
||||
need to setup again the folder name at the Session level.
|
||||
Set the folderName for logging, reports, context and data output
|
||||
*/
|
||||
let config
|
||||
if (bot.LEARNING_SESSION.config !== undefined) {
|
||||
config = bot.LEARNING_SESSION.config
|
||||
if (config.folderName === undefined) {
|
||||
bot.LEARNING_SESSION.folderName = bot.processNode.session.type.replace(' ', '-').replace(' ', '-') + '-' + bot.LEARNING_SESSION.id
|
||||
} else {
|
||||
bot.LEARNING_SESSION.folderName = bot.processNode.session.type.replace(' ', '-').replace(' ', '-') + '-' + bot.processNode.session.config.folderName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkParemeters() {
|
||||
/*
|
||||
Here we check all the Session Parameters received. If something critical is missing we abort returning false. If something
|
||||
non critical is missing, we complete it with a default value.
|
||||
*/
|
||||
|
||||
if (bot.LEARNING_SESSION.learningParameters === undefined) {
|
||||
let errorMessage = "Session Node with no Parameters."
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] initialize -> checkParemeters -> " + errorMessage)
|
||||
bot.sessionError(bot.LEARNING_SESSION, errorMessage)
|
||||
return false
|
||||
}
|
||||
|
||||
/* Time Range */
|
||||
if (bot.LEARNING_SESSION.learningParameters.timeRange === undefined) { // if the Time Range is missing we create a default one.
|
||||
bot.LEARNING_SESSION.learningParameters.timeRange = {
|
||||
name: 'Missing Time Range',
|
||||
type: 'Time Range',
|
||||
config: {
|
||||
initialDatetime: (new Date()).valueOf(),
|
||||
finalDatetime: (new Date()).valueOf() + global.ONE_YEAR_IN_MILISECONDS
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/* Check that we received valid dates */
|
||||
if (bot.LEARNING_SESSION.type === 'Backtesting Session') {
|
||||
if (isNaN(new Date(bot.LEARNING_SESSION.learningParameters.timeRange.config.initialDatetime)).valueOf()) {
|
||||
let errorMessage = "sessionParameters.timeRange.config.initialDatetime is not a valid date."
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] initialize -> checkParemeters -> " + errorMessage)
|
||||
bot.sessionError(bot.LEARNING_SESSION.learningParameters, errorMessage)
|
||||
return false
|
||||
}
|
||||
}
|
||||
if (isNaN(new Date(bot.LEARNING_SESSION.learningParameters.timeRange.config.finalDatetime)).valueOf()) {
|
||||
let errorMessage = "sessionParameters.timeRange.config.initialDatetime is not a valid date."
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] initialize -> checkParemeters -> " + errorMessage)
|
||||
bot.sessionError(bot.LEARNING_SESSION.learningParameters, errorMessage)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/* Session Type Forced Values */
|
||||
let today = (new Date()).valueOf()
|
||||
let aYearAgo = today - global.ONE_YEAR_IN_MILISECONDS
|
||||
let aYearFromNow = today + global.ONE_YEAR_IN_MILISECONDS
|
||||
switch (bot.LEARNING_SESSION.type) {
|
||||
case 'Backtesting Session': {
|
||||
useDefaultDatetimes(aYearAgo, today)
|
||||
break
|
||||
}
|
||||
case 'Live Trading Session': {
|
||||
useDefaultDatetimes(today, aYearFromNow)
|
||||
break
|
||||
}
|
||||
case 'Fordward Testing Session': {
|
||||
useDefaultDatetimes(today, aYearFromNow)
|
||||
break
|
||||
}
|
||||
case 'Paper Trading Session': {
|
||||
useDefaultDatetimes(today, aYearFromNow)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function useDefaultDatetimes(initialDefault, finalDefault) {
|
||||
/*
|
||||
Note that inside the system, we are going to deal with these
|
||||
dates in their numeric value representation.
|
||||
*/
|
||||
|
||||
/* Initial Datetime */
|
||||
if (bot.LEARNING_SESSION.type === 'Backtesting Session') {
|
||||
if (bot.LEARNING_SESSION.learningParameters.timeRange.config.initialDatetime === undefined) {
|
||||
bot.LEARNING_SESSION.learningParameters.timeRange.config.initialDatetime = initialDefault
|
||||
} else {
|
||||
bot.LEARNING_SESSION.learningParameters.timeRange.config.initialDatetime = (new Date(bot.LEARNING_SESSION.learningParameters.timeRange.config.initialDatetime)).valueOf()
|
||||
}
|
||||
} else {
|
||||
/* Non backtest session can start from the past only if explicitly configured that way */
|
||||
if (bot.LEARNING_SESSION.learningParameters.timeRange.config.initialDatetime === undefined || bot.LEARNING_SESSION.learningParameters.timeRange.config.allowStartingFromThePast !== true) {
|
||||
bot.LEARNING_SESSION.learningParameters.timeRange.config.initialDatetime = initialDefault
|
||||
} else {
|
||||
bot.LEARNING_SESSION.learningParameters.timeRange.config.initialDatetime = (new Date(bot.LEARNING_SESSION.learningParameters.timeRange.config.initialDatetime)).valueOf()
|
||||
}
|
||||
}
|
||||
|
||||
/* Final Datetime */
|
||||
if (bot.LEARNING_SESSION.learningParameters.timeRange.config.finalDatetime === undefined) {
|
||||
bot.LEARNING_SESSION.learningParameters.timeRange.config.finalDatetime = finalDefault
|
||||
} else {
|
||||
bot.LEARNING_SESSION.learningParameters.timeRange.config.finalDatetime = (new Date(bot.LEARNING_SESSION.learningParameters.timeRange.config.finalDatetime)).valueOf()
|
||||
}
|
||||
}
|
||||
|
||||
/* Time Frame */
|
||||
if (bot.LEARNING_SESSION.learningParameters.timeFrame === undefined) {
|
||||
let errorMessage = "Session Parameters Node with no Time Frame."
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] initialize -> checkParemeters -> " + errorMessage)
|
||||
bot.sessionError(bot.LEARNING_SESSION.learningParameters, errorMessage)
|
||||
return false
|
||||
}
|
||||
if (bot.LEARNING_SESSION.learningParameters.timeFrame.config.label === undefined) {
|
||||
let errorMessage = "Session Parameters Node with no Time Frame Label configuration."
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] initialize -> checkParemeters -> " + errorMessage)
|
||||
bot.sessionError(bot.LEARNING_SESSION.learningParameters.timeFrame, errorMessage)
|
||||
return false
|
||||
}
|
||||
bot.LEARNING_SESSION.learningParameters.timeFrame.config.value = getTimeFrameFromLabel(bot.LEARNING_SESSION.learningParameters.timeFrame.config.label)
|
||||
if (bot.LEARNING_SESSION.learningParameters.timeFrame.config.value === undefined) {
|
||||
let errorMessage = "Config error: label value not recognized. Try 01-min or 01-hs for example."
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] initialize -> checkParemeters -> " + errorMessage)
|
||||
bot.sessionError(bot.LEARNING_SESSION.learningParameters.timeFrame, errorMessage)
|
||||
return false
|
||||
}
|
||||
|
||||
/* Session Base Asset */
|
||||
if (bot.LEARNING_SESSION.learningParameters.sessionBaseAsset === undefined) {
|
||||
let errorMessage = "Session Parameters Node with no Session Base Asset."
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] initialize -> checkParemeters -> " + errorMessage)
|
||||
bot.sessionError(bot.LEARNING_SESSION.learningParameters, errorMessage)
|
||||
return false
|
||||
}
|
||||
if (bot.LEARNING_SESSION.learningParameters.sessionBaseAsset.config.initialBalance === undefined) {
|
||||
let errorMessage = "Session Parameters Session Base Asset with no initialBalance configuration."
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] initialize -> checkParemeters -> " + errorMessage)
|
||||
bot.sessionError(bot.LEARNING_SESSION.learningParameters.sessionBaseAsset, errorMessage)
|
||||
return false
|
||||
}
|
||||
|
||||
/* Session Quoted Asset */
|
||||
if (bot.LEARNING_SESSION.learningParameters.sessionQuotedAsset === undefined) {
|
||||
let errorMessage = "Session Parameters Node with no Session Quoted Asset."
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] initialize -> checkParemeters -> " + errorMessage)
|
||||
bot.sessionError(bot.LEARNING_SESSION.learningParameters, errorMessage)
|
||||
return false
|
||||
}
|
||||
if (bot.LEARNING_SESSION.learningParameters.sessionQuotedAsset.config.initialBalance === undefined) {
|
||||
let errorMessage = "Session Parameters Session Quoted Asset with no initialBalance configuration."
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] initialize -> checkParemeters -> " + errorMessage)
|
||||
bot.sessionError(bot.LEARNING_SESSION.learningParameters.sessionQuotedAsset, errorMessage)
|
||||
return false
|
||||
}
|
||||
|
||||
/* Slippage */
|
||||
if (bot.LEARNING_SESSION.learningParameters.slippage === undefined) { // if the Slippage is missing we create a default one.
|
||||
bot.LEARNING_SESSION.learningParameters.slippage = {
|
||||
name: 'Missing Slippage',
|
||||
type: 'Slippage',
|
||||
config: {
|
||||
marketOrderRate: 0,
|
||||
stopLoss: 0,
|
||||
takeProfit: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
if (bot.LEARNING_SESSION.learningParameters.slippage.config.marketOrderRate === undefined) { // if the marketOrderRate is missing we create a default one.
|
||||
bot.LEARNING_SESSION.learningParameters.slippage.config.marketOrderRate = 0
|
||||
}
|
||||
if (bot.LEARNING_SESSION.learningParameters.slippage.config.stopLoss === undefined) { // if the stopLoss is missing we create a default one.
|
||||
bot.LEARNING_SESSION.learningParameters.slippage.config.stopLoss = 0
|
||||
}
|
||||
if (bot.LEARNING_SESSION.learningParameters.slippage.config.takeProfit === undefined) { // if the takeProfit is missing we create a default one.
|
||||
bot.LEARNING_SESSION.learningParameters.slippage.config.takeProfit = 0
|
||||
}
|
||||
|
||||
/* Fee Structure */
|
||||
if (bot.LEARNING_SESSION.learningParameters.feeStructure === undefined) { // if the Fee Structure is missing we create a default one.
|
||||
bot.LEARNING_SESSION.learningParameters.feeStructure = {
|
||||
name: 'Missing Fee Structure',
|
||||
type: 'Fee Structure',
|
||||
config: {
|
||||
maker: 0,
|
||||
taker: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
if (bot.LEARNING_SESSION.learningParameters.feeStructure.config.maker === undefined) { // if the maker is missing we create a default one.
|
||||
bot.LEARNING_SESSION.learningParameters.feeStructure.config.maker = 0
|
||||
}
|
||||
if (bot.LEARNING_SESSION.learningParameters.feeStructure.config.taker === undefined) { // if the taker is missing we create a default one.
|
||||
bot.LEARNING_SESSION.learningParameters.feeStructure.config.taker = 0
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function startBackTesting(message) {
|
||||
return true
|
||||
}
|
||||
|
||||
function startLiveTrading(message) {
|
||||
if (bot.KEY === undefined || bot.SECRET === undefined) {
|
||||
let errorMessage = "Key 'codeName' or 'secret' not provided. Plese check that and try again."
|
||||
if (FULL_LOG === true) { parentLogger.write(MODULE_NAME, "[ERROR] initialize -> startLiveTrading -> " + errorMessage); }
|
||||
bot.sessionError(bot.LEARNING_SESSION, errorMessage)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function startFordwardTesting(message) {
|
||||
if (bot.KEY === undefined || bot.SECRET === undefined) {
|
||||
if (FULL_LOG === true) { parentLogger.write(MODULE_NAME, "[WARN] initialize -> startFordwardTesting -> Key name or Secret not provided, not possible to run the process in Forward Testing mode."); }
|
||||
console.log("Key 'codeName' or 'secret' not provided. Plese check that and try again.")
|
||||
return false
|
||||
}
|
||||
|
||||
/* Reduce the balance */
|
||||
let balancePercentage = 1 // This is the default value
|
||||
|
||||
if (bot.LEARNING_SESSION.config.balancePercentage !== undefined) {
|
||||
balancePercentage = bot.LEARNING_SESSION.config.balancePercentage
|
||||
}
|
||||
|
||||
bot.LEARNING_SESSION.learningParameters.sessionBaseAsset.config.initialBalance = bot.LEARNING_SESSION.learningParameters.sessionBaseAsset.config.initialBalance * balancePercentage / 100
|
||||
bot.LEARNING_SESSION.learningParameters.sessionQuotedAsset.config.initialBalance = bot.LEARNING_SESSION.learningParameters.sessionQuotedAsset.config.initialBalance * balancePercentage / 100
|
||||
|
||||
bot.LEARNING_SESSION.learningParameters.sessionBaseAsset.config.minimumBalance = bot.LEARNING_SESSION.learningParameters.sessionBaseAsset.config.minimumBalance * balancePercentage / 100
|
||||
bot.LEARNING_SESSION.learningParameters.sessionQuotedAsset.config.minimumBalance = bot.LEARNING_SESSION.learningParameters.sessionQuotedAsset.config.minimumBalance * balancePercentage / 100
|
||||
|
||||
bot.LEARNING_SESSION.learningParameters.sessionBaseAsset.config.maximumBalance = bot.LEARNING_SESSION.learningParameters.sessionBaseAsset.config.maximumBalance * balancePercentage / 100
|
||||
bot.LEARNING_SESSION.learningParameters.sessionQuotedAsset.config.maximumBalance = bot.LEARNING_SESSION.learningParameters.sessionQuotedAsset.config.maximumBalance * balancePercentage / 100
|
||||
|
||||
pProcessConfig.normalWaitTime = bot.LEARNING_SESSION.learningParameters.timeFrame.config.value
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function startPaperTrading(message) {
|
||||
return true
|
||||
}
|
||||
|
||||
function getTimeFrameFromLabel(timeFrameLabel) {
|
||||
|
||||
for (let i = 0; i < global.marketFilesPeriods.length; i++) {
|
||||
let value = global.marketFilesPeriods[i][0]
|
||||
let label = global.marketFilesPeriods[i][1]
|
||||
|
||||
if (timeFrameLabel === label) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < global.dailyFilePeriods.length; i++) {
|
||||
let value = global.dailyFilePeriods[i][0]
|
||||
let label = global.dailyFilePeriods[i][1]
|
||||
|
||||
if (timeFrameLabel === label) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sessionHeartBeat(processingDate, percentage, status) {
|
||||
let event = {
|
||||
seconds: (new Date()).getSeconds(),
|
||||
processingDate: processingDate,
|
||||
percentage: percentage,
|
||||
status: status
|
||||
}
|
||||
global.EVENT_SERVER_CLIENT.raiseEvent(bot.sessionKey, 'Heartbeat', event)
|
||||
|
||||
if (global.STOP_TASK_GRACEFULLY === true) {
|
||||
bot.STOP_SESSION = true
|
||||
if (FULL_LOG === true) { parentLogger.write(MODULE_NAME, '[IMPORTANT] sessionHeartBeat -> Stopping the Session now. ') }
|
||||
}
|
||||
}
|
||||
|
||||
function sessionError(node, errorMessage) {
|
||||
let event
|
||||
if (node !== undefined) {
|
||||
event = {
|
||||
nodeName: node.name,
|
||||
nodeType: node.type,
|
||||
nodeId: node.id,
|
||||
errorMessage: errorMessage
|
||||
}
|
||||
} else {
|
||||
event = {
|
||||
errorMessage: errorMessage
|
||||
}
|
||||
}
|
||||
global.EVENT_SERVER_CLIENT.raiseEvent(bot.sessionKey, 'Error', event)
|
||||
|
||||
if (global.STOP_TASK_GRACEFULLY === true) {
|
||||
bot.STOP_SESSION = true
|
||||
if (FULL_LOG === true) { parentLogger.write(MODULE_NAME, '[IMPORTANT] sessionError -> Stopping the Session now. ') }
|
||||
}
|
||||
}
|
||||
|
||||
function sessionWarning(node, warningMessage) {
|
||||
let event
|
||||
if (node !== undefined) {
|
||||
event = {
|
||||
nodeName: node.name,
|
||||
nodeType: node.type,
|
||||
nodeId: node.id,
|
||||
warningMessage: warningMessage
|
||||
}
|
||||
} else {
|
||||
event = {
|
||||
warningMessage: warningMessage
|
||||
}
|
||||
}
|
||||
global.EVENT_SERVER_CLIENT.raiseEvent(bot.sessionKey, 'Warning', event)
|
||||
|
||||
if (global.STOP_TASK_GRACEFULLY === true) {
|
||||
bot.STOP_SESSION = true
|
||||
if (FULL_LOG === true) { parentLogger.write(MODULE_NAME, '[IMPORTANT] sessionWarning -> Stopping the Session now. ') }
|
||||
}
|
||||
}
|
||||
|
||||
function sessionInfo(node, infoMessage) {
|
||||
let event
|
||||
if (node !== undefined) {
|
||||
event = {
|
||||
nodeName: node.name,
|
||||
nodeType: node.type,
|
||||
nodeId: node.id,
|
||||
infoMessage: infoMessage
|
||||
}
|
||||
} else {
|
||||
event = {
|
||||
infoMessage: infoMessage
|
||||
}
|
||||
}
|
||||
global.EVENT_SERVER_CLIENT.raiseEvent(bot.sessionKey, 'Info', event)
|
||||
|
||||
if (global.STOP_TASK_GRACEFULLY === true) {
|
||||
bot.STOP_SESSION = true
|
||||
if (FULL_LOG === true) { parentLogger.write(MODULE_NAME, '[IMPORTANT] sessionInfo -> Stopping the Session now. ') }
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] initialize -> err = " + err.stack);
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
exports.newAnnouncements = function newAnnouncements(bot, logger) {
|
||||
|
||||
const MODULE_NAME = 'Announcements'
|
||||
|
||||
let thisObject = {
|
||||
makeAnnoucements: makeAnnoucements,
|
||||
initialize: initialize,
|
||||
finalize: finalize
|
||||
}
|
||||
|
||||
let tradingSystem
|
||||
let tradingEngine
|
||||
|
||||
return thisObject
|
||||
|
||||
function initialize() {
|
||||
tradingSystem = bot.simulationState.tradingSystem
|
||||
tradingEngine = bot.simulationState.tradingEngine
|
||||
}
|
||||
|
||||
function finalize() {
|
||||
tradingSystem = undefined
|
||||
tradingEngine = undefined
|
||||
bot.TRADING_SESSION.messagesSent = undefined
|
||||
}
|
||||
|
||||
function makeAnnoucements(node) {
|
||||
if (node.announcements !== undefined) {
|
||||
for (let i = 0; i < node.announcements.length; i++) {
|
||||
let announcement = node.announcements[i]
|
||||
let canAnnounce = true
|
||||
if (announcement.announcementCondition !== undefined) {
|
||||
let conditionValue
|
||||
try {
|
||||
conditionValue = eval(announcement.announcementCondition.code)
|
||||
} catch (err) {
|
||||
tradingSystem.errors.push([announcement.announcementCondition.id, err.message])
|
||||
}
|
||||
if (conditionValue !== true) { canAnnounce = false }
|
||||
}
|
||||
if (canAnnounce === true) {
|
||||
let text = announcement.config.text
|
||||
if (announcement.formula !== undefined) {
|
||||
text = tradingSystem.formulas.get(announcement.formula.id)
|
||||
}
|
||||
|
||||
if (bot.TRADING_SESSION.socialBots !== undefined) {
|
||||
bot.TRADING_SESSION.socialBots.announce(text)
|
||||
tradingSystem.announcements.push([announcement.id, text])
|
||||
} else {
|
||||
tradingSystem.errors.push([announcement.id, 'Could not announce because session does not have Social Bots.'])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
exports.newExchangeAPI = function newExchangeAPI(bot, logger) {
|
||||
|
||||
let MODULE_NAME = "Exchange API";
|
||||
|
||||
let thisObject = {
|
||||
getOrder: getOrder,
|
||||
createOrder: createOrder,
|
||||
cancelOrder: cancelOrder,
|
||||
initialize: initialize,
|
||||
finalize: finalize
|
||||
};
|
||||
|
||||
let tradingSystem
|
||||
|
||||
let exchangeId
|
||||
let options = {}
|
||||
let exchange
|
||||
|
||||
const ccxt = require('ccxt')
|
||||
|
||||
return thisObject;
|
||||
|
||||
function initialize() {
|
||||
tradingSystem = bot.simulationState.tradingSystem
|
||||
|
||||
exchangeId = bot.exchange.toLowerCase()
|
||||
|
||||
let key = bot.KEY
|
||||
let secret = bot.SECRET
|
||||
|
||||
const exchangeClass = ccxt[exchangeId]
|
||||
const exchangeConstructorParams = {
|
||||
'apiKey': key,
|
||||
'secret': secret,
|
||||
'timeout': 30000,
|
||||
'enableRateLimit': true,
|
||||
verbose: false,
|
||||
'adjustForTimeDifference': true,
|
||||
options: options
|
||||
}
|
||||
exchange = new exchangeClass(exchangeConstructorParams)
|
||||
}
|
||||
|
||||
function finalize() {
|
||||
tradingSystem = undefined
|
||||
exchangeId = undefined
|
||||
options = undefined
|
||||
exchange = undefined
|
||||
}
|
||||
|
||||
async function getOrder(tradingSystemOrder, tradingEngineOrder) {
|
||||
|
||||
let orderId = tradingEngineOrder.exchangeId.value
|
||||
const symbol = bot.market.baseAsset + '/' + bot.market.quotedAsset
|
||||
|
||||
/* Basic Logging */
|
||||
logInfo("getOrder -> orderId = " + orderId)
|
||||
logInfo("getOrder -> symbol = " + symbol)
|
||||
|
||||
/* Basic Validations */
|
||||
if (exchange.has['fetchOrder'] === false) {
|
||||
logError("getOrder -> Exchange does not support fetchOrder command.");
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
let order = await exchange.fetchOrder(orderId, symbol)
|
||||
|
||||
logInfo("getOrder -> Response from the Exchange: " + JSON.stringify(order));
|
||||
return order
|
||||
} catch (err) {
|
||||
tradingSystem.errors.push([tradingSystemOrder.id, "getOrder -> Error = " + err.message])
|
||||
logError("getOrder -> Error = " + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function createOrder(tradingSystemOrder, tradingEngineOrder) {
|
||||
|
||||
let price = tradingEngineOrder.rate.value // CCXT: how much quote currency you are willing to pay for a trade lot of base currency (for limit orders only)
|
||||
let type // CCXT: a string literal type of order, ccxt currently unifies market and limit orders only
|
||||
let side // CCXT: a string literal for the direction of your order, buy or sell
|
||||
let symbol = bot.market.baseAsset + '/' + bot.market.quotedAsset // CCXT: a string literal symbol of the market you wish to trade on, like BTC/USD, ZEC/ETH, DOGE/DASH, etc
|
||||
let amount = tradingEngineOrder.orderBaseAsset.size.value // CCXT: how much of currency you want to trade, in Base Asset.
|
||||
|
||||
switch (tradingSystemOrder.type) {
|
||||
case 'Market Buy Order': {
|
||||
type = 'market'
|
||||
side = 'buy'
|
||||
break
|
||||
}
|
||||
case 'Market Sell Order': {
|
||||
type = 'market'
|
||||
side = 'sell'
|
||||
break
|
||||
}
|
||||
case 'Limit Buy Order': {
|
||||
type = 'limit'
|
||||
side = 'buy'
|
||||
break
|
||||
}
|
||||
case 'Limit Sell Order': {
|
||||
type = 'limit'
|
||||
side = 'sell'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/* Basic Logging */
|
||||
logInfo("createOrder -> symbol = " + symbol);
|
||||
logInfo("createOrder -> side = " + side);
|
||||
logInfo("createOrder -> type = " + type);
|
||||
logInfo("createOrder -> amount = " + amount);
|
||||
logInfo("createOrder -> price = " + price);
|
||||
|
||||
/* Basic Validations */
|
||||
if (side !== "buy" && side !== "sell") {
|
||||
logError("createOrder -> side must be either 'buy' or 'sell'.");
|
||||
return
|
||||
}
|
||||
if (exchange.has['createOrder'] === false) {
|
||||
logError("createOrder -> Exchange does not support createOrder command.");
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
let order = await (exchange.createOrder(symbol, type, side, amount, price))
|
||||
|
||||
logInfo("createOrder -> Response from the Exchange: " + JSON.stringify(order));
|
||||
return order.id
|
||||
} catch (err) {
|
||||
tradingSystem.errors.push([tradingSystemOrder.id, "createOrder -> Error = " + err.message])
|
||||
logError("createOrder -> Error = " + err.message)
|
||||
logError("createOrder -> symbol = " + symbol);
|
||||
logError("createOrder -> side = " + side);
|
||||
logError("createOrder -> type = " + type);
|
||||
logError("createOrder -> amount = " + amount);
|
||||
logError("createOrder -> price = " + price);
|
||||
if (
|
||||
err.message.indexOf('API-key format invalid') >= 0 ||
|
||||
err.message.indexOf('Invalid API-key, IP, or permissions for action.') >= 0 ||
|
||||
err.message.indexOf('"code":-2015') >= 0
|
||||
) {
|
||||
logError ('The exchange says the API key provided is not good or it is not in the correct format. This is what you are using:')
|
||||
errorFooter()
|
||||
}
|
||||
if (
|
||||
err.message.indexOf('Signature for this request is not valid.') >= 0 ||
|
||||
err.message.indexOf('"code":-1022') >= 0
|
||||
) {
|
||||
logError ('The exchange says the secret provided is not good, incorrect or not in the correct format. This is what you are using:')
|
||||
errorFooter()
|
||||
}
|
||||
function errorFooter(){
|
||||
logError ('key: ->' + bot.KEY + '<-')
|
||||
logError ('secret: ->' + bot.SECRET + '<-')
|
||||
logError ('key.length:' + bot.KEY.length )
|
||||
logError ('secret.length:' + bot.SECRET.length )
|
||||
logError ('Double check that you copied the key at the codeName property and the secret at secret property without bringing any invisible caracter from the exchange web site, or leaving any character from the sample text. Also check that the keys are not disabled at the exchange ot that they are not restricted by IP.')
|
||||
logError ('As a remainder. Binance key and secret are 64 bytes in lenght each one. ' )
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function cancelOrder(tradingSystemOrder, tradingEngineOrder) {
|
||||
|
||||
let orderId = tradingEngineOrder.exchangeId.value
|
||||
|
||||
/* Basic Logging */
|
||||
logInfo("cancelOrder -> Entering function. orderId = " + orderId);
|
||||
|
||||
const symbol = bot.market.baseAsset + '/' + bot.market.quotedAsset
|
||||
|
||||
/* Basic Validations */
|
||||
if (exchange.has['fetchOrder'] === false) {
|
||||
logError("cancelOrder -> Exchange does not support fetchOrder command.");
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
let order = await exchange.cancelOrder(orderId, symbol)
|
||||
|
||||
logInfo("cancelOrder -> Response from the Exchange: " + JSON.stringify(order));
|
||||
return true
|
||||
} catch (err) {
|
||||
tradingSystem.errors.push([tradingSystemOrder.id, "cancelOrder -> Error = " + err.message])
|
||||
logError("cancelOrder -> Error = " + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
function logInfo(message) {
|
||||
logger.write(MODULE_NAME, '[INFO] ' + message)
|
||||
}
|
||||
|
||||
function logError(message) {
|
||||
logger.write(MODULE_NAME, '[ERROR] ' + message)
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,316 @@
|
||||
exports.newOrdersCalculations = function newOrdersCalculations(bot, logger) {
|
||||
/*
|
||||
When we are live trading, we need to syncronize with the exchange.
|
||||
*/
|
||||
const MODULE_NAME = 'Orders Calculations'
|
||||
|
||||
let thisObject = {
|
||||
actualSizeCalculation: actualSizeCalculation,
|
||||
actualRateCalculation: actualRateCalculation,
|
||||
feesToBePaidCalculation: feesToBePaidCalculation,
|
||||
amountReceivedCalculation: amountReceivedCalculation,
|
||||
percentageFilledCalculation: percentageFilledCalculation,
|
||||
feesPaidCalculation: feesPaidCalculation,
|
||||
sizeFilledCalculation: sizeFilledCalculation,
|
||||
initialize: initialize,
|
||||
finalize: finalize
|
||||
}
|
||||
|
||||
let tradingEngine
|
||||
let tradingSystem
|
||||
let sessionParameters
|
||||
|
||||
return thisObject
|
||||
|
||||
function initialize() {
|
||||
sessionParameters = bot.TRADING_SESSION.tradingParameters
|
||||
tradingEngine = bot.simulationState.tradingEngine
|
||||
tradingSystem = bot.simulationState.tradingSystem
|
||||
}
|
||||
|
||||
function finalize() {
|
||||
sessionParameters = undefined
|
||||
tradingEngine = undefined
|
||||
tradingSystem = undefined
|
||||
}
|
||||
|
||||
async function actualSizeCalculation(tradingEngineStage, tradingSystemOrder, tradingEngineOrder, order, applyFeePercentage) {
|
||||
/*
|
||||
When we submit the order to the exchange, we do it specifying the order size
|
||||
in Base Asset. It is mandatory to be in Base Asset as per CCXT Library API and
|
||||
probably as per the Exchange API too.
|
||||
|
||||
The exchange might take that size or might change it a little
|
||||
bit. For example, if it does not accept as many decimals as we are sending.
|
||||
|
||||
We have also seen that for Market Orders, the exchange usually does not take
|
||||
the size requested exactly. Sometimes is a little bit less or even a little bit
|
||||
more. For Limit orders it is usually the same.
|
||||
|
||||
If it happens that the size accepted (Actual Size) is different than the
|
||||
size requested (Size), we need to make several adjustments so that the
|
||||
accounting syncronizes with reality.
|
||||
*/
|
||||
|
||||
/* This calculation needs to happen only once, the first time the order is checked. */
|
||||
if (tradingEngineOrder.orderBaseAsset.actualSize.value !== tradingEngineOrder.orderBaseAsset.actualSize.config.initialValue) { return }
|
||||
|
||||
/* We receive the actual size from the exchange at the order.amount field. */
|
||||
tradingEngineOrder.orderBaseAsset.actualSize.value = order.amount
|
||||
|
||||
recalculateActualSize()
|
||||
recalculateSizePlaced()
|
||||
|
||||
function recalculateActualSize() {
|
||||
/*
|
||||
We will recalculate the Quoted Asset Actual Size. This will also give to it an initial value.
|
||||
In the next formula, we are unsing the rate of the order because we dont know yet the Actual Rate.
|
||||
This rate might be replaced afteerwards for the Actual Rate when this is calculated again once
|
||||
the Actual Rate is known.
|
||||
*/
|
||||
tradingEngineOrder.orderQuotedAsset.actualSize.value = tradingEngineOrder.orderBaseAsset.actualSize.value * tradingEngineOrder.rate.value
|
||||
|
||||
tradingEngineOrder.orderBaseAsset.actualSize.value = global.PRECISE(tradingEngineOrder.orderBaseAsset.actualSize.value, 10)
|
||||
tradingEngineOrder.orderQuotedAsset.actualSize.value = global.PRECISE(tradingEngineOrder.orderQuotedAsset.actualSize.value, 10)
|
||||
}
|
||||
|
||||
function recalculateSizePlaced() {
|
||||
/*
|
||||
We will also recalculate the Size Placed that was previously calculated with the order Size,
|
||||
and now we will do it with the order Actual Size.
|
||||
|
||||
We need to unaccount the previous size placed and correctly account
|
||||
for the the new actual sizes we now know.
|
||||
*/
|
||||
tradingEngineStage.stageBaseAsset.sizePlaced.value =
|
||||
tradingEngineStage.stageBaseAsset.sizePlaced.value -
|
||||
tradingEngineOrder.orderBaseAsset.size.value
|
||||
|
||||
tradingEngineStage.stageBaseAsset.sizePlaced.value =
|
||||
tradingEngineStage.stageBaseAsset.sizePlaced.value +
|
||||
tradingEngineOrder.orderBaseAsset.actualSize.value
|
||||
|
||||
tradingEngineStage.stageQuotedAsset.sizePlaced.value =
|
||||
tradingEngineStage.stageQuotedAsset.sizePlaced.value -
|
||||
tradingEngineOrder.orderQuotedAsset.size.value
|
||||
|
||||
tradingEngineStage.stageQuotedAsset.sizePlaced.value =
|
||||
tradingEngineStage.stageQuotedAsset.sizePlaced.value +
|
||||
tradingEngineOrder.orderQuotedAsset.actualSize.value
|
||||
|
||||
tradingEngineStage.stageBaseAsset.sizePlaced.value = global.PRECISE(tradingEngineStage.stageBaseAsset.sizePlaced.value, 10)
|
||||
tradingEngineStage.stageQuotedAsset.sizePlaced.value = global.PRECISE(tradingEngineStage.stageQuotedAsset.sizePlaced.value, 10)
|
||||
}
|
||||
}
|
||||
|
||||
async function actualRateCalculation(tradingEngineStage, tradingSystemOrder, tradingEngineOrder, order, applyFeePercentage) {
|
||||
/*
|
||||
Actual Rate Calculation: Let's remember that the exchange may not take the rate
|
||||
we send it with the order (in Limit orders) and for any reason change it a little bit.
|
||||
For example we might have sent a rate with too many decimals and the exchange truncate
|
||||
them. Another thing that might happen is that we send a rate that is matching orders already
|
||||
at the order book. In this case we would get an actual rate not only different than the one
|
||||
we sent, but also, better since the trading engine inside the exchange would match orders
|
||||
with the first orders best by price.
|
||||
|
||||
In the case of Market Orders we don't even send a rate. For all those reason
|
||||
we get and store the Actual Rate at which the order was filled.
|
||||
*/
|
||||
if (order.average !== undefined) {
|
||||
/*
|
||||
We will use the average whenever is available. As of today it is not 100% clear when this is
|
||||
available, but it seems sometimes it is not. In those cases we use the price.
|
||||
*/
|
||||
tradingEngineOrder.orderStatistics.actualRate.value = order.average
|
||||
} else {
|
||||
/*
|
||||
We use the order.price when the average is not available.
|
||||
*/
|
||||
tradingEngineOrder.orderStatistics.actualRate.value = order.price
|
||||
}
|
||||
|
||||
tradingEngineOrder.orderStatistics.actualRate.value = global.PRECISE(tradingEngineOrder.orderStatistics.actualRate.value, 10)
|
||||
|
||||
/*
|
||||
If the Actual Rate happens to be exctly the same than the order rate, then there is
|
||||
nothing else to do here. Otherwise there are adjustments to be made.
|
||||
*/
|
||||
if (tradingEngineOrder.orderStatistics.actualRate.value === tradingEngineOrder.rate.value) { return }
|
||||
|
||||
let previousQuotedAssetActualSize = tradingEngineOrder.orderQuotedAsset.actualSize.value
|
||||
|
||||
recalculateActualSize()
|
||||
recalculateSizePlaced()
|
||||
|
||||
function recalculateActualSize() {
|
||||
if (tradingEngineOrder.orderStatistics.actualRate.value === tradingEngineOrder.rate.value) { return }
|
||||
/*
|
||||
Now we know the Actual Rate at which the order was filled. Since the actual rate
|
||||
is not the same as the Rate we defined for the order, we need to syncronize
|
||||
the Actual Order Size for Quoted Asset since it was calculated with the Order Size that we
|
||||
now know it is not the one really used at the exchange. We will recalculate the
|
||||
Actual Size in Quoted Asset and not in Base Asset, since the Actual Size in Base Asset
|
||||
is the one we accepted by the exchange, so that is fixed, regardless of how we initially
|
||||
got the Order Size in Base Asset, either from direct imput from the user or calculated
|
||||
from the Order Size in Quoted Asset input it by the user. So here we go.
|
||||
*/
|
||||
previousQuotedAssetActualSize = tradingEngineOrder.orderQuotedAsset.actualSize.value
|
||||
|
||||
tradingEngineOrder.orderQuotedAsset.actualSize.value =
|
||||
tradingEngineOrder.orderBaseAsset.actualSize.value *
|
||||
tradingEngineOrder.orderStatistics.actualRate.value
|
||||
|
||||
tradingEngineOrder.orderQuotedAsset.actualSize.value = global.PRECISE(tradingEngineOrder.orderQuotedAsset.actualSize.value, 10)
|
||||
|
||||
tradingSystem.warnings.push(
|
||||
[
|
||||
[tradingEngineOrder.orderQuotedAsset.actualSize.id, tradingEngineOrder.orderStatistics.actualRate.id],
|
||||
'Actual Size (' + previousQuotedAssetActualSize + ') recalculated (' + tradingEngineOrder.orderQuotedAsset.actualSize.value + ') because the Actual Rate (' + tradingEngineOrder.orderStatistics.actualRate.value + ') is different than the Order Rate (' + tradingEngineOrder.rate.value + '))'
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
function recalculateSizePlaced() {
|
||||
let previousStageQuotedAssetSizePlaced = tradingEngineStage.stageQuotedAsset.sizePlaced.value
|
||||
/*
|
||||
Since we changed the Actual Size in Quoted Asset, we need to fix the Size Placed in Quoted Asset
|
||||
that was previously calculated with the previous value of actualSize.
|
||||
*/
|
||||
tradingEngineStage.stageQuotedAsset.sizePlaced.value =
|
||||
tradingEngineStage.stageQuotedAsset.sizePlaced.value -
|
||||
previousQuotedAssetActualSize
|
||||
|
||||
tradingEngineStage.stageQuotedAsset.sizePlaced.value =
|
||||
tradingEngineStage.stageQuotedAsset.sizePlaced.value +
|
||||
tradingEngineOrder.orderQuotedAsset.actualSize.value
|
||||
|
||||
tradingEngineStage.stageQuotedAsset.sizePlaced.value = global.PRECISE(tradingEngineStage.stageQuotedAsset.sizePlaced.value, 10)
|
||||
|
||||
tradingSystem.warnings.push(
|
||||
[
|
||||
[tradingEngineStage.stageQuotedAsset.sizePlaced.id, tradingEngineOrder.orderStatistics.actualRate.id],
|
||||
'Size Placed (' + previousStageQuotedAssetSizePlaced + ') recalculated (' + tradingEngineStage.stageQuotedAsset.sizePlaced.value + ') because the Actual Rate (' + tradingEngineOrder.orderStatistics.actualRate.value + ') is different than the Order Rate (' + tradingEngineOrder.rate.value + '))'
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function feesToBePaidCalculation(tradingEngineStage, tradingSystemOrder, tradingEngineOrder, order, applyFeePercentage) {
|
||||
/*
|
||||
The Fees To Be Paid are the fees that will be paid once the order is 100% Filled.
|
||||
*/
|
||||
switch (tradingEngineOrder.type) {
|
||||
case 'Market Order': {
|
||||
await applyFeePercentage(
|
||||
'feesToBePaid',
|
||||
tradingEngineOrder,
|
||||
tradingSystemOrder,
|
||||
sessionParameters.feeStructure.config.taker,
|
||||
100
|
||||
)
|
||||
break
|
||||
}
|
||||
case 'Limit Order': {
|
||||
await applyFeePercentage(
|
||||
'feesToBePaid',
|
||||
tradingEngineOrder,
|
||||
tradingSystemOrder,
|
||||
sessionParameters.feeStructure.config.maker,
|
||||
100
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function percentageFilledCalculation(tradingEngineStage, tradingSystemOrder, tradingEngineOrder, order, applyFeePercentage) {
|
||||
/*
|
||||
Percentage Filled Calculation: The only relevant information we get from the exchange is the order.filled.
|
||||
We know this field will go up until reaching the Actual Size. Once it reaches that number
|
||||
the order will be 100% filled.
|
||||
*/
|
||||
tradingEngineOrder.orderStatistics.percentageFilled.value = order.filled * 100 / tradingEngineOrder.orderBaseAsset.actualSize.value
|
||||
tradingEngineOrder.orderStatistics.percentageFilled.value = global.PRECISE(tradingEngineOrder.orderStatistics.percentageFilled.value, 10)
|
||||
}
|
||||
|
||||
async function feesPaidCalculation(tradingEngineStage, tradingSystemOrder, tradingEngineOrder, order, applyFeePercentage) {
|
||||
/*
|
||||
Within the order information received from the exchange we can not see the fees paid so we need to
|
||||
calculate them ourselves. Knwowing the Fees Paid is critical to later update the balances correctly,
|
||||
since at every trade we will receive less than just the Actual Size. In fact we will receive the Actual
|
||||
Size minus the Fees Paid.
|
||||
|
||||
The percentage at which the exchange will charge fees needs to be configured at the the Session Fees Parameters.
|
||||
*/
|
||||
switch (tradingEngineOrder.type) {
|
||||
case 'Market Order': {
|
||||
await applyFeePercentage(
|
||||
'feesPaid',
|
||||
tradingEngineOrder,
|
||||
tradingSystemOrder,
|
||||
sessionParameters.feeStructure.config.taker,
|
||||
tradingEngineOrder.orderStatistics.percentageFilled.value
|
||||
)
|
||||
break
|
||||
}
|
||||
case 'Limit Order': {
|
||||
await applyFeePercentage(
|
||||
'feesPaid',
|
||||
tradingEngineOrder,
|
||||
tradingSystemOrder,
|
||||
sessionParameters.feeStructure.config.maker,
|
||||
tradingEngineOrder.orderStatistics.percentageFilled.value
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function sizeFilledCalculation(tradingEngineStage, tradingSystemOrder, tradingEngineOrder, order, applyFeePercentage) {
|
||||
/*
|
||||
CCXT returns order.filled with an amount denominated in Base Asset. We will
|
||||
take it from there for our Order Base Asset. The amount in order.filled does
|
||||
not reflect the Fees Paid, even if we are paying the fees in Base Asset.
|
||||
*/
|
||||
tradingEngineOrder.orderBaseAsset.sizeFilled.value = order.filled
|
||||
tradingEngineOrder.orderBaseAsset.sizeFilled.value = global.PRECISE(tradingEngineOrder.orderBaseAsset.sizeFilled.value, 10)
|
||||
/*
|
||||
For Quoted Asset, CCXT does not return any field with the size filled, so we
|
||||
need to calculate that by ourselves.
|
||||
*/
|
||||
tradingEngineOrder.orderQuotedAsset.sizeFilled.value =
|
||||
tradingEngineOrder.orderQuotedAsset.actualSize.value * tradingEngineOrder.orderStatistics.percentageFilled.value / 100
|
||||
|
||||
tradingEngineOrder.orderQuotedAsset.sizeFilled.value = global.PRECISE(tradingEngineOrder.orderQuotedAsset.sizeFilled.value, 10)
|
||||
}
|
||||
|
||||
async function amountReceivedCalculation(tradingEngineStage, tradingSystemOrder, tradingEngineOrder, order, applyFeePercentage) {
|
||||
/*
|
||||
This calculation is for informational purposes, so that users do not have to calculate
|
||||
it by themselves. The Amount Received out of the trade depends if we are Buying or Selling
|
||||
the Base Asset. If we are Buying, then the Amount Receiced will be in Base Asset. If we
|
||||
are selling then it will be in Quoted Asset.
|
||||
*/
|
||||
|
||||
switch (true) {
|
||||
case tradingSystemOrder.type === 'Market Buy Order' || tradingSystemOrder.type === 'Limit Buy Order': {
|
||||
/*
|
||||
In this case the Amount Received is in Base Asset.
|
||||
*/
|
||||
tradingEngineOrder.orderBaseAsset.amountReceived.value =
|
||||
tradingEngineOrder.orderBaseAsset.sizeFilled.value -
|
||||
tradingEngineOrder.orderBaseAsset.feesPaid.value
|
||||
break
|
||||
}
|
||||
case tradingSystemOrder.type === 'Market Sell Order' || tradingSystemOrder.type === 'Limit Sell Order': {
|
||||
/*
|
||||
In this case the Amount Received is in Quoted Asset.
|
||||
*/
|
||||
tradingEngineOrder.orderQuotedAsset.amountReceived.value =
|
||||
tradingEngineOrder.orderQuotedAsset.sizeFilled.value -
|
||||
tradingEngineOrder.orderQuotedAsset.feesPaid.value
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
exports.newOrdersSimulations = function newOrdersSimulations(bot, logger) {
|
||||
/*
|
||||
When we are backtesting or paper trading, we need to simulate what would have happened at the exchange.
|
||||
*/
|
||||
const MODULE_NAME = 'Orders Simulations'
|
||||
|
||||
let thisObject = {
|
||||
actualSizeSimulation: actualSizeSimulation,
|
||||
actualRateSimulation: actualRateSimulation,
|
||||
feesToBePaidSimulation: feesToBePaidSimulation,
|
||||
amountReceivedSimulation: amountReceivedSimulation,
|
||||
percentageFilledSimulation: percentageFilledSimulation,
|
||||
feesPaidSimulation: feesPaidSimulation,
|
||||
sizeFilledSimulation: sizeFilledSimulation,
|
||||
initialize: initialize,
|
||||
finalize: finalize
|
||||
}
|
||||
|
||||
let tradingEngine
|
||||
let tradingSystem
|
||||
let sessionParameters
|
||||
|
||||
return thisObject
|
||||
|
||||
function initialize() {
|
||||
sessionParameters = bot.TRADING_SESSION.tradingParameters
|
||||
tradingEngine = bot.simulationState.tradingEngine
|
||||
tradingSystem = bot.simulationState.tradingSystem
|
||||
}
|
||||
|
||||
function finalize() {
|
||||
sessionParameters = undefined
|
||||
tradingEngine = undefined
|
||||
tradingSystem = undefined
|
||||
}
|
||||
|
||||
async function actualSizeSimulation(tradingEngineStage, tradingSystemOrder, tradingEngineOrder, applyFeePercentage) {
|
||||
/*
|
||||
The Actual Size is the size the exchange accepts for the order,
|
||||
regarless of the size sent in the request to the exchange.
|
||||
|
||||
While backtesting and paper trading, we do not really need to
|
||||
simulate an Actual Size. We will just copy to that field the
|
||||
size of the order.
|
||||
|
||||
This calculation needs to happen only once, so as to set these values.
|
||||
*/
|
||||
if (tradingEngineOrder.orderBaseAsset.actualSize.value !== tradingEngineOrder.orderBaseAsset.actualSize.config.initialValue) { return }
|
||||
|
||||
tradingEngineOrder.orderBaseAsset.actualSize.value = tradingEngineOrder.orderBaseAsset.size.value
|
||||
tradingEngineOrder.orderQuotedAsset.actualSize.value = tradingEngineOrder.orderQuotedAsset.size.value
|
||||
}
|
||||
|
||||
function actualRateSimulation(tradingEngineStage, tradingSystemOrder, tradingEngineOrder, applyFeePercentage) {
|
||||
/* Actual Rate Simulation */
|
||||
let previousQuotedAssetActualSize = tradingEngineOrder.orderQuotedAsset.actualSize.value
|
||||
|
||||
if (tradingEngineOrder.orderStatistics.actualRate.value === tradingEngineOrder.orderStatistics.actualRate.config.initialValue) {
|
||||
/*
|
||||
The default value for the Actual Rate is the rate of the order.
|
||||
*/
|
||||
tradingEngineOrder.orderStatistics.actualRate.value = tradingEngineOrder.rate.value
|
||||
}
|
||||
|
||||
basedOnSessionParameters()
|
||||
basedOnTradingSystem()
|
||||
considerBestMatchWithOrderBook()
|
||||
recalculateActualSize()
|
||||
recalculateSizePlaced()
|
||||
|
||||
function basedOnSessionParameters() {
|
||||
/*
|
||||
The default way to calculate the Actual Rate is to apply the Slippage defined
|
||||
at the Session Parameters Definition. We are not going to apply the default if there
|
||||
is a definition for this order on how to calculate the actual rate.
|
||||
*/
|
||||
if (tradingSystemOrder.simulatedExchangeEvents !== undefined) {
|
||||
if (tradingSystemOrder.simulatedExchangeEvents.simulatedActualRate !== undefined) {
|
||||
return
|
||||
}
|
||||
}
|
||||
switch (tradingEngineOrder.type) {
|
||||
case 'Market Order': {
|
||||
|
||||
/* Actual Rate is simulated based on the Session Paremeters */
|
||||
let slippageAmount = tradingEngineOrder.rate.value * sessionParameters.slippage.config.marketOrderRate / 100
|
||||
if (slippageAmount !== 0) {
|
||||
switch (tradingSystemOrder.type) {
|
||||
case 'Market Sell Order': {
|
||||
tradingEngineOrder.orderStatistics.actualRate.value = tradingEngineOrder.rate.value - slippageAmount
|
||||
tradingEngineOrder.orderStatistics.actualRate.value = global.PRECISE(tradingEngineOrder.orderStatistics.actualRate.value, 10)
|
||||
|
||||
tradingSystem.warnings.push(
|
||||
[
|
||||
[sessionParameters.slippage.id, tradingEngineOrder.rate.id, tradingEngineOrder.orderStatistics.actualRate.id],
|
||||
'slippage.config.marketOrderRate Slippage Amount (' + slippageAmount + ') substracted from Order Rate (' + tradingEngineOrder.rate.value + ') to get the Actual Rate (' + tradingEngineOrder.orderStatistics.actualRate.value + ')'
|
||||
]
|
||||
)
|
||||
break
|
||||
}
|
||||
case 'Market Buy Order': {
|
||||
tradingEngineOrder.orderStatistics.actualRate.value = tradingEngineOrder.rate.value + slippageAmount
|
||||
tradingEngineOrder.orderStatistics.actualRate.value = global.PRECISE(tradingEngineOrder.orderStatistics.actualRate.value, 10)
|
||||
|
||||
tradingSystem.warnings.push(
|
||||
[
|
||||
[sessionParameters.slippage.id, tradingEngineOrder.rate.id, tradingEngineOrder.orderStatistics.actualRate.id],
|
||||
'slippage.config.marketOrderRate Slippage Amount (' + slippageAmount + ') added to Order Rate (' + tradingEngineOrder.rate.value + ') to get the Actual Rate (' + tradingEngineOrder.orderStatistics.actualRate.value + ')'
|
||||
]
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'Limit Order': {
|
||||
/* In Limit Orders the actual rate is the rate of the order, there is no slippage */
|
||||
tradingEngineOrder.orderStatistics.actualRate.value = tradingEngineOrder.rate.value
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function basedOnTradingSystem() {
|
||||
/*
|
||||
If the user specified a Formula to simulate the Actual Rate for this order,
|
||||
we use that formula here. Note that if this Formula exists, this will override
|
||||
the calculation based on Slippage.
|
||||
*/
|
||||
if (tradingSystemOrder.simulatedExchangeEvents !== undefined) {
|
||||
if (tradingSystemOrder.simulatedExchangeEvents.simulatedActualRate !== undefined) {
|
||||
if (tradingSystemOrder.simulatedExchangeEvents.simulatedActualRate.formula !== undefined) {
|
||||
/* Calculate this only once for this order */
|
||||
if (tradingEngineOrder.orderStatistics.actualRate.value === tradingEngineOrder.orderStatistics.actualRate.config.initialValue) {
|
||||
let newValue = tradingSystem.formulas.get(tradingSystemOrder.simulatedExchangeEvents.simulatedActualRate.formula.id)
|
||||
newValue = global.PRECISE(newValue, 10)
|
||||
|
||||
tradingSystem.warnings.push(
|
||||
[
|
||||
tradingSystemOrder.simulatedExchangeEvents.simulatedActualRate.formula.id,
|
||||
'Actual Rate (' + tradingEngineOrder.orderStatistics.actualRate.value + ') calculated with this Formula (' + newValue + ')'
|
||||
]
|
||||
)
|
||||
tradingEngineOrder.orderStatistics.actualRate.value = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function considerBestMatchWithOrderBook() {
|
||||
/*
|
||||
Until here we have got the Actual Rate based on the formula definition or based on session parameters slippage.
|
||||
The last check is about watching what happened in the market. Let's remember that the exchange will
|
||||
fill the order with the best possible matches at it's order book. That means that if the rate
|
||||
we set for the order was too low (for a sale order) or too hight (for a buy order), the actual rate
|
||||
should be better than expected.
|
||||
|
||||
We don't know what it is at the order book, but whe know that the last candle includes trades that bounced between
|
||||
the min an max of the candle. That could tell us the above the max and below the min, the order would be matched
|
||||
with orders at the order book. For that reason, we do this:
|
||||
*/
|
||||
switch (true) {
|
||||
case tradingSystemOrder.type === 'Market Buy Order' || tradingSystemOrder.type === 'Limit Buy Order': {
|
||||
if (tradingEngineOrder.orderStatistics.actualRate.value > tradingEngine.current.episode.candle.max.value) {
|
||||
tradingSystem.warnings.push(
|
||||
[
|
||||
[tradingEngineOrder.orderStatistics.actualRate.id, tradingEngine.current.episode.candle.max.id],
|
||||
'Actual Rate (' + tradingEngineOrder.orderStatistics.actualRate.value + ') too high. Changed to candle.max (' + tradingEngine.current.episode.candle.max.value + ')'
|
||||
]
|
||||
)
|
||||
tradingEngineOrder.orderStatistics.actualRate.value = tradingEngine.current.episode.candle.max.value
|
||||
}
|
||||
break
|
||||
}
|
||||
case tradingSystemOrder.type === 'Market Sell Order' || tradingSystemOrder.type === 'Limit Sell Order': {
|
||||
if (tradingEngineOrder.orderStatistics.actualRate.value < tradingEngine.current.episode.candle.min.value) {
|
||||
tradingSystem.warnings.push(
|
||||
[
|
||||
[tradingEngineOrder.orderStatistics.actualRate.id, tradingEngine.current.episode.candle.min.id],
|
||||
'Actual Rate (' + tradingEngineOrder.orderStatistics.actualRate.value + ') too low. Changed to candle.min (' + tradingEngine.current.episode.candle.max.value + ')'
|
||||
]
|
||||
)
|
||||
tradingEngineOrder.orderStatistics.actualRate.value = tradingEngine.current.episode.candle.min.value
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
tradingEngineOrder.orderStatistics.actualRate.value = global.PRECISE(tradingEngineOrder.orderStatistics.actualRate.value, 10)
|
||||
}
|
||||
|
||||
function recalculateActualSize() {
|
||||
if (tradingEngineOrder.orderStatistics.actualRate.value === tradingEngineOrder.rate.value) { return }
|
||||
/*
|
||||
Now that we have an Actual Rate, we need to adjust the Actual Size in Quoted Asset, since
|
||||
Base Asset is what the exchange accepts as input and give information at. Quotes Asset properties are always calculated.
|
||||
*/
|
||||
tradingEngineOrder.orderQuotedAsset.actualSize.value =
|
||||
tradingEngineOrder.orderBaseAsset.actualSize.value *
|
||||
tradingEngineOrder.orderStatistics.actualRate.value
|
||||
|
||||
tradingEngineOrder.orderQuotedAsset.actualSize.value = global.PRECISE(tradingEngineOrder.orderQuotedAsset.actualSize.value, 10)
|
||||
|
||||
tradingSystem.warnings.push(
|
||||
[
|
||||
[tradingEngineOrder.orderQuotedAsset.actualSize.id, tradingEngineOrder.orderStatistics.actualRate.id],
|
||||
'Actual Size (' + previousQuotedAssetActualSize + ') recalculated (' + tradingEngineOrder.orderQuotedAsset.actualSize.value + ') because the Actual Rate (' + tradingEngineOrder.orderStatistics.actualRate.value + ') is different than the Order Rate (' + tradingEngineOrder.rate.value + '))'
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
function recalculateSizePlaced() {
|
||||
if (tradingEngineOrder.orderStatistics.actualRate.value === tradingEngineOrder.rate.value) { return }
|
||||
/*
|
||||
Since the Actual Rate might have changed, we need to recalculate the Size Placed, where we accumulate all the Size Placed of
|
||||
all orders of a Stage. For Base Asset there is nothing to do, since the Actual Rate does not have an inpact on it.
|
||||
|
||||
For Quoted Asset, we need to first unaccount what this same orded added before to Size Placed (with the precious Actual Size) and
|
||||
account with the new Actual Size.
|
||||
*/
|
||||
let previousStageQuotedAssetSizePlaced = tradingEngineStage.stageQuotedAsset.sizePlaced.value
|
||||
|
||||
tradingEngineStage.stageQuotedAsset.sizePlaced.value =
|
||||
tradingEngineStage.stageQuotedAsset.sizePlaced.value -
|
||||
previousQuotedAssetActualSize
|
||||
|
||||
tradingEngineStage.stageQuotedAsset.sizePlaced.value =
|
||||
tradingEngineStage.stageQuotedAsset.sizePlaced.value +
|
||||
tradingEngineOrder.orderQuotedAsset.actualSize.value
|
||||
|
||||
tradingEngineStage.stageQuotedAsset.sizePlaced.value = global.PRECISE(tradingEngineStage.stageQuotedAsset.sizePlaced.value, 10)
|
||||
|
||||
tradingSystem.warnings.push(
|
||||
[
|
||||
[tradingEngineStage.stageQuotedAsset.sizePlaced.id, tradingEngineOrder.orderStatistics.actualRate.id],
|
||||
'Size Placed (' + previousStageQuotedAssetSizePlaced + ') recalculated (' + tradingEngineStage.stageQuotedAsset.sizePlaced.value + ') because the Actual Rate (' + tradingEngineOrder.orderStatistics.actualRate.value + ') is different than the Order Rate (' + tradingEngineOrder.rate.value + '))'
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function feesToBePaidSimulation(tradingEngineStage, tradingSystemOrder, tradingEngineOrder, applyFeePercentage) {
|
||||
/*
|
||||
The Fees To Be Paid are the fees that will be paid once the order is 100% Filled.
|
||||
*/
|
||||
switch (tradingEngineOrder.type) {
|
||||
case 'Market Order': {
|
||||
await applyFeePercentage(
|
||||
'feesToBePaid',
|
||||
tradingEngineOrder,
|
||||
tradingSystemOrder,
|
||||
sessionParameters.feeStructure.config.taker,
|
||||
100
|
||||
)
|
||||
break
|
||||
}
|
||||
case 'Limit Order': {
|
||||
await applyFeePercentage(
|
||||
'feesToBePaid',
|
||||
tradingEngineOrder,
|
||||
tradingSystemOrder,
|
||||
sessionParameters.feeStructure.config.maker,
|
||||
100
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function percentageFilledSimulation(tradingEngineStage, tradingSystemOrder, tradingEngineOrder, applyFeePercentage) {
|
||||
/*
|
||||
When we are live, it is the exchange who knows which order was filled and in
|
||||
which amount. Since we are backtesting here, we need to simulate what happens
|
||||
at the exchange. We can not simulate that an order is filled if the candle we
|
||||
see did not touch the order.
|
||||
|
||||
For limit buy orders we are going to check if the price was below the order actual rate, and
|
||||
for limit sell orders we are going to check if the price was above the order actual rate.
|
||||
|
||||
If we have a partial filling defined specifically for this order,
|
||||
we are going to use it to estimate how much of the order we will consider filled.
|
||||
*/
|
||||
let orderWasHit
|
||||
switch (tradingSystemOrder.type) {
|
||||
case 'Limit Buy Order': {
|
||||
if (tradingEngine.current.episode.candle.min.value <= tradingEngineOrder.orderStatistics.actualRate.value) {
|
||||
orderWasHit = true
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'Limit Sell Order': {
|
||||
if (tradingEngine.current.episode.candle.max.value >= tradingEngineOrder.orderStatistics.actualRate.value) {
|
||||
orderWasHit = true
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'Market Buy Order': {
|
||||
orderWasHit = true
|
||||
break
|
||||
}
|
||||
case 'Market Sell Order': {
|
||||
orderWasHit = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (orderWasHit === true) {
|
||||
/*
|
||||
Initially, we will assume that 100% of the order was filled. This might be overwritten
|
||||
if there is a configuration for this order that details how much is must be considered
|
||||
filled each time we check this.
|
||||
*/
|
||||
tradingEngineOrder.orderStatistics.percentageFilled.value = 100
|
||||
|
||||
/* Based on the Trading System Definition */
|
||||
if (tradingSystemOrder.simulatedExchangeEvents !== undefined) {
|
||||
if (tradingSystemOrder.simulatedExchangeEvents.simulatedPartialFill !== undefined) {
|
||||
if (tradingSystemOrder.simulatedExchangeEvents.simulatedPartialFill.config.fillProbability !== undefined) {
|
||||
|
||||
/* Percentage Filled */
|
||||
let percentageFilled = tradingSystemOrder.simulatedExchangeEvents.simulatedPartialFill.config.fillProbability * 100
|
||||
if (tradingEngineOrder.orderStatistics.percentageFilled.value + percentageFilled > 100) {
|
||||
percentageFilled = 100 - tradingEngineOrder.orderStatistics.percentageFilled.value
|
||||
}
|
||||
tradingEngineOrder.orderStatistics.percentageFilled.value = tradingEngineOrder.orderStatistics.percentageFilled.value + percentageFilled
|
||||
tradingEngineOrder.orderStatistics.percentageFilled.value = global.PRECISE(tradingEngineOrder.orderStatistics.percentageFilled.value, 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function feesPaidSimulation(tradingEngineStage, tradingSystemOrder, tradingEngineOrder, applyFeePercentage) {
|
||||
/*
|
||||
The way the Fess Paid are calculated is as follows: From the order Actual Size we apply the % of fee,
|
||||
that would give us the total fees for that order. To that we apply the % filled of the orders,
|
||||
that give use the final result of the fees paid for the current % filled.
|
||||
|
||||
But before that, we will see if there is an specific definition on how to simulate the fees for
|
||||
this order, or if there is not, we use the Fees Structure defined at the Session Parameters.
|
||||
*/
|
||||
|
||||
/* Based on the Trading System Definition */
|
||||
if (tradingSystemOrder.simulatedExchangeEvents !== undefined) {
|
||||
if (tradingSystemOrder.simulatedExchangeEvents.simulatedFeesPaid !== undefined) {
|
||||
if (tradingSystemOrder.simulatedExchangeEvents.simulatedFeesPaid.config.percentage !== undefined) {
|
||||
applyFeePercentage(
|
||||
'feesPaid',
|
||||
tradingEngineOrder,
|
||||
tradingSystemOrder,
|
||||
tradingSystemOrder.simulatedExchangeEvents.simulatedFeesPaid.config.percentage,
|
||||
tradingEngineOrder.orderStatistics.percentageFilled.value
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Another way to simulate the Fees Paid is by using the Session Paremeters configuration for Fees.
|
||||
*/
|
||||
switch (tradingEngineOrder.type) {
|
||||
case 'Market Order': {
|
||||
applyFeePercentage(
|
||||
'feesPaid',
|
||||
tradingEngineOrder,
|
||||
tradingSystemOrder,
|
||||
sessionParameters.feeStructure.config.taker,
|
||||
tradingEngineOrder.orderStatistics.percentageFilled.value
|
||||
)
|
||||
break
|
||||
}
|
||||
case 'Limit Order': {
|
||||
applyFeePercentage(
|
||||
'feesPaid',
|
||||
tradingEngineOrder,
|
||||
tradingSystemOrder,
|
||||
sessionParameters.feeStructure.config.maker,
|
||||
tradingEngineOrder.orderStatistics.percentageFilled.value
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sizeFilledSimulation(tradingEngineStage, tradingSystemOrder, tradingEngineOrder, applyFeePercentage) {
|
||||
/*
|
||||
Size Filled is the amount of Base Asset or Quoted Asset that at the exchange matches
|
||||
another counterparty order. We calculate this using the Percentage Filled we already simulated.
|
||||
*/
|
||||
|
||||
tradingEngineOrder.orderBaseAsset.sizeFilled.value =
|
||||
tradingEngineOrder.orderBaseAsset.actualSize.value *
|
||||
tradingEngineOrder.orderStatistics.percentageFilled.value / 100
|
||||
|
||||
tradingEngineOrder.orderQuotedAsset.sizeFilled.value =
|
||||
tradingEngineOrder.orderQuotedAsset.actualSize.value *
|
||||
tradingEngineOrder.orderStatistics.percentageFilled.value / 100
|
||||
|
||||
tradingEngineOrder.orderBaseAsset.sizeFilled.value = global.PRECISE(tradingEngineOrder.orderBaseAsset.sizeFilled.value, 10)
|
||||
tradingEngineOrder.orderQuotedAsset.sizeFilled.value = global.PRECISE(tradingEngineOrder.orderQuotedAsset.sizeFilled.value, 10)
|
||||
}
|
||||
|
||||
async function amountReceivedSimulation(tradingEngineStage, tradingSystemOrder, tradingEngineOrder, order, applyFeePercentage) {
|
||||
/*
|
||||
This simulation is for informational purposes, so that users do not have to calculate
|
||||
it by themselves. The Amount Received out of the trade depends if we are Buying or Selling
|
||||
the Base Asset. If we are Buying, then the Amount Receiced will be in Base Asset. If we
|
||||
are selling then it will be in Quoted Asset.
|
||||
*/
|
||||
|
||||
switch (true) {
|
||||
case tradingSystemOrder.type === 'Market Buy Order' || tradingSystemOrder.type === 'Limit Buy Order': {
|
||||
/*
|
||||
In this case the Amount Received is in Base Asset.
|
||||
*/
|
||||
tradingEngineOrder.orderBaseAsset.amountReceived.value =
|
||||
tradingEngineOrder.orderBaseAsset.sizeFilled.value -
|
||||
tradingEngineOrder.orderBaseAsset.feesPaid.value
|
||||
break
|
||||
}
|
||||
case tradingSystemOrder.type === 'Market Sell Order' || tradingSystemOrder.type === 'Limit Sell Order': {
|
||||
/*
|
||||
In this case the Amount Received is in Quoted Asset.
|
||||
*/
|
||||
tradingEngineOrder.orderQuotedAsset.amountReceived.value =
|
||||
tradingEngineOrder.orderQuotedAsset.sizeFilled.value -
|
||||
tradingEngineOrder.orderQuotedAsset.feesPaid.value
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
exports.newSnapshots = function newSnapshots(bot, logger) {
|
||||
/*
|
||||
This module encapsulates the snapshots functionality.
|
||||
*/
|
||||
const MODULE_NAME = 'Snapshots'
|
||||
|
||||
let thisObject = {
|
||||
updateChart: updateChart,
|
||||
strategyEntry: strategyEntry,
|
||||
strategyExit: strategyExit,
|
||||
positionEntry: positionEntry,
|
||||
positionExit: positionExit,
|
||||
addCodeToSnapshot: addCodeToSnapshot,
|
||||
initialize: initialize,
|
||||
finalize: finalize
|
||||
}
|
||||
|
||||
let tradingEngine
|
||||
let tradingSystem
|
||||
let sessionParameters
|
||||
|
||||
const SNAPSHOTS_FOLDER_NAME = 'Snapshots'
|
||||
|
||||
/* Snapshots of Strategies and Positions */
|
||||
let snapshots = {
|
||||
headers: [],
|
||||
strategies: [],
|
||||
positions: []
|
||||
}
|
||||
|
||||
let positionKeys = new Map() // This is to prevent adding the same indicator / variable to the output collection.
|
||||
let strategyKeys = new Map()
|
||||
let createHeaders = true // This is going to be true only once
|
||||
let createCloseHeaders = true // This is going to be true only once
|
||||
let closeValues
|
||||
let strategyValues
|
||||
let addToStrategyValues
|
||||
let positionValues
|
||||
let addToPositionValues
|
||||
|
||||
let chart
|
||||
|
||||
return thisObject
|
||||
|
||||
function initialize() {
|
||||
tradingEngine = bot.simulationState.tradingEngine
|
||||
tradingSystem = bot.simulationState.tradingSystem
|
||||
sessionParameters = bot.TRADING_SESSION.tradingParameters
|
||||
}
|
||||
|
||||
function finalize() {
|
||||
writeSnapshotFiles()
|
||||
tradingEngine = undefined
|
||||
tradingSystem = undefined
|
||||
sessionParameters = undefined
|
||||
|
||||
positionKeys = undefined
|
||||
strategyKeys = undefined
|
||||
|
||||
chart = undefined
|
||||
}
|
||||
|
||||
function updateChart(pChart) {
|
||||
chart = pChart
|
||||
}
|
||||
|
||||
function strategyEntry() {
|
||||
/*
|
||||
We will record here a snapshot of all values used in conditions and formulas within the Trading System.
|
||||
If the strategy exists with a trade, this information will be added to the snapshots file.
|
||||
*/
|
||||
|
||||
strategyKeys = new Map()
|
||||
strategyValues = []
|
||||
addToStrategyValues = true
|
||||
|
||||
evalNode(tradingSystem)
|
||||
|
||||
/* Turn off this */
|
||||
createHeaders = false
|
||||
addToStrategyValues = false
|
||||
}
|
||||
|
||||
function strategyExit() {
|
||||
getResults(tradingEngine.current.strategy.begin.value, tradingEngine.current.strategy.end.value)
|
||||
let valuesArray = closeValues.concat(strategyValues)
|
||||
snapshots.strategies.push(valuesArray)
|
||||
}
|
||||
|
||||
function positionEntry() {
|
||||
|
||||
positionKeys = new Map()
|
||||
positionValues = []
|
||||
addToPositionValues = true
|
||||
|
||||
evalNode(tradingSystem)
|
||||
|
||||
/* Turn off this */
|
||||
createHeaders = false
|
||||
addToPositionValues = false
|
||||
}
|
||||
|
||||
function positionExit() {
|
||||
getResults(tradingEngine.current.position.begin.value, tradingEngine.current.position.end.value)
|
||||
let valuesArray = closeValues.concat(positionValues)
|
||||
snapshots.positions.push(valuesArray)
|
||||
}
|
||||
|
||||
function evalNode(node) {
|
||||
if (node === undefined) { return }
|
||||
|
||||
/* Here we check if there is a codition to be evaluated */
|
||||
if (node.type === 'Condition') {
|
||||
/* We will eval this condition */
|
||||
evalCondition(node)
|
||||
}
|
||||
|
||||
/* Here we check if there is a formula to be evaluated */
|
||||
if (node.type === 'Formula') {
|
||||
if (node.code !== undefined) {
|
||||
/* We will eval this formula */
|
||||
evalFormula(node)
|
||||
}
|
||||
}
|
||||
|
||||
/* Now we go down through all this node children */
|
||||
let nodeDefinition = global.APP_SCHEMA_MAP.get(node.type)
|
||||
if (nodeDefinition === undefined) { return }
|
||||
|
||||
if (nodeDefinition.properties !== undefined) {
|
||||
let previousPropertyName // Since there are cases where there are many properties with the same name,because they can hold nodes of different types but only one at the time, we have to avoind counting each property of those as individual children.
|
||||
for (let i = 0; i < nodeDefinition.properties.length; i++) {
|
||||
let property = nodeDefinition.properties[i]
|
||||
|
||||
switch (property.type) {
|
||||
case 'node': {
|
||||
if (property.name !== previousPropertyName) {
|
||||
if (node[property.name] !== undefined) {
|
||||
evalNode(node[property.name])
|
||||
}
|
||||
previousPropertyName = property.name
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'array': {
|
||||
if (node[property.name] !== undefined) {
|
||||
let nodePropertyArray = node[property.name]
|
||||
for (let m = 0; m < nodePropertyArray.length; m++) {
|
||||
evalNode(nodePropertyArray[m])
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function evalCondition(node) {
|
||||
|
||||
/*
|
||||
The code can be at the condition node if it was done with the Conditions Editor, or it can also be
|
||||
at a Javascript Code node. If there is a Javascript object we will give it preference and take the code
|
||||
from there. Otherwise we will take the code from the Condition node.
|
||||
*/
|
||||
|
||||
let nodeWithCode = node
|
||||
if (node.javascriptCode !== undefined) {
|
||||
if (node.javascriptCode.code !== undefined) {
|
||||
nodeWithCode = node.javascriptCode
|
||||
}
|
||||
}
|
||||
addCodeToSnapshot(nodeWithCode)
|
||||
}
|
||||
|
||||
function evalFormula(node) {
|
||||
let nodeWithCode = node
|
||||
addCodeToSnapshot(nodeWithCode)
|
||||
}
|
||||
|
||||
function addCodeToSnapshot(nodeWithCode) {
|
||||
if (nodeWithCode === undefined) { return }
|
||||
if (nodeWithCode.code === undefined) { return }
|
||||
|
||||
try {
|
||||
let instructionsArray = nodeWithCode.code.split(' ')
|
||||
for (let i = 0; i < instructionsArray.length; i++) {
|
||||
let instruction = instructionsArray[i]
|
||||
instruction = instruction.replace('(', '')
|
||||
instruction = instruction.replace(')', '')
|
||||
instruction = instruction.replace(/</g, '')
|
||||
instruction = instruction.replace(/>/g, '')
|
||||
instruction = instruction.replace(/<=/g, '')
|
||||
instruction = instruction.replace(/>=/g, '')
|
||||
instruction = instruction.replace(/!=/g, '')
|
||||
instruction = instruction.replace(/!==/g, '')
|
||||
instruction = instruction.replace(/==/g, '')
|
||||
instruction = instruction.replace(/===/g, '')
|
||||
instruction = instruction.replace(/{/g, '')
|
||||
instruction = instruction.replace(/}/g, '')
|
||||
if (instruction.indexOf('chart') >= 0) {
|
||||
let parts = instruction.split('.')
|
||||
let timeFrame = parts[1]
|
||||
let product = parts[2]
|
||||
let property
|
||||
checkPrevious(3)
|
||||
function checkPrevious(index) {
|
||||
property = parts[index]
|
||||
if (property === 'previous') {
|
||||
product = product + '.previous'
|
||||
checkPrevious(index + 1)
|
||||
}
|
||||
}
|
||||
|
||||
// Example: chart.at01hs.popularSMA.sma200 - chart.at01hs.popularSMA.sma100 < 10
|
||||
if (timeFrame !== 'atAnyTimeFrame') {
|
||||
timeFrame = timeFrame.substring(2, 4) + '-' + timeFrame.substring(4, 7)
|
||||
}
|
||||
let key = timeFrame + '-' + product + '-' + property
|
||||
let existingKey
|
||||
|
||||
if (addToStrategyValues === true) {
|
||||
addValues(strategyKeys, strategyValues)
|
||||
}
|
||||
|
||||
if (addToPositionValues === true) {
|
||||
addValues(positionKeys, positionValues)
|
||||
}
|
||||
|
||||
function addValues(keys, values) {
|
||||
existingKey = keys.get(key)
|
||||
|
||||
if (existingKey === undefined) { // means that at the current loop this property of this product was not used before.
|
||||
keys.set(key, key)
|
||||
if (createHeaders === true) {
|
||||
snapshots.headers.push(key)
|
||||
}
|
||||
|
||||
try {
|
||||
let value = eval(instruction)
|
||||
values.push(value)
|
||||
} catch (err) {
|
||||
tradingSystem.errors.push([nodeWithCode.id, 'Instruction ' + instruction + ' evalueates with error ' + err.message])
|
||||
values.push(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
tradingSystem.errors.push([nodeWithCode.id, err.message])
|
||||
logger.write(MODULE_NAME, '[ERROR] runSimulation -> addCodeToSnapshot -> nodeWithCode.code = ' + nodeWithCode.code)
|
||||
logger.write(MODULE_NAME, '[ERROR] runSimulation -> addCodeToSnapshot -> err = ' + err.stack)
|
||||
}
|
||||
}
|
||||
|
||||
function getResults(openDatetime, closeDatetime) {
|
||||
|
||||
let closeHeaders = ['Trade Number', 'Open Datetime', 'Close Datetime', 'Strategy Name', 'Trigger On Situation', 'Take Position Situation', 'Result In Base Asset', 'Result In Quoted Asset', 'ROI in Base Asset', 'ROI in Quoted Asset', 'Exit Type']
|
||||
closeValues = [
|
||||
tradingEngine.current.episode.episodeCounters.positions.value, // Position Number
|
||||
(new Date(openDatetime)).toISOString(), // Open Datetime
|
||||
(new Date(closeDatetime)).toISOString(), // Open Datetime
|
||||
tradingEngine.current.strategy.strategyName.value, // Strategy Name
|
||||
tradingEngine.current.strategy.situationName.value, // Trigger On Situation
|
||||
tradingEngine.current.position.situationName.value, // Take Position Situation
|
||||
tradingEngine.current.position.positionBaseAsset.hitFail.value, // Result in Base Asset
|
||||
tradingEngine.current.position.positionQuotedAsset.hitFail.value, // Result in Base Asset
|
||||
tradingEngine.current.position.positionBaseAsset.ROI.value, // ROI in Base Asseet
|
||||
tradingEngine.current.position.positionQuotedAsset.ROI.value, // ROI in Quoted Asset
|
||||
tradingEngine.current.position.exitType.value // Exit Type
|
||||
]
|
||||
|
||||
if (createCloseHeaders === true) {
|
||||
snapshots.headers = closeHeaders.concat(JSON.parse(JSON.stringify(snapshots.headers)))
|
||||
createCloseHeaders = false
|
||||
}
|
||||
}
|
||||
|
||||
function writeSnapshotFiles() {
|
||||
|
||||
if (sessionParameters.snapshots !== undefined) {
|
||||
if (sessionParameters.snapshots.config.strategy === true) {
|
||||
writeSnapshotFile(snapshots.strategies, 'Strategies')
|
||||
}
|
||||
}
|
||||
|
||||
if (sessionParameters.snapshots !== undefined) {
|
||||
if (sessionParameters.snapshots.config.strategy === true) {
|
||||
writeSnapshotFile(snapshots.positions, 'Positions')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function writeSnapshotFile(snapshotArray, pFileName) {
|
||||
try {
|
||||
const FILE_STORAGE = require('../FileStorage.js');
|
||||
let fileStorage = FILE_STORAGE.newFileStorage(logger);
|
||||
|
||||
let fileContent = "";
|
||||
let separator = "\r\n";
|
||||
|
||||
parseRecord(snapshots.headers)
|
||||
|
||||
for (let i = 0; i < snapshotArray.length; i++) {
|
||||
let record = snapshotArray[i];
|
||||
parseRecord(record)
|
||||
}
|
||||
|
||||
function parseRecord(record) {
|
||||
for (let j = 0; j < record.length; j++) {
|
||||
let property = record[j]
|
||||
|
||||
fileContent = fileContent + '' + property
|
||||
if (j !== record.length - 1) {
|
||||
fileContent = fileContent + ","
|
||||
}
|
||||
}
|
||||
fileContent = fileContent + separator
|
||||
}
|
||||
|
||||
fileContent = "" + fileContent + "";
|
||||
|
||||
let fileName = pFileName + '.csv';
|
||||
let filePath = bot.filePathRoot + "/Output/" + bot.TRADING_SESSION.folderName + "/" + SNAPSHOTS_FOLDER_NAME;
|
||||
filePath += '/' + fileName
|
||||
|
||||
fileStorage.createTextFile(filePath, fileContent + '\n', onFileCreated);
|
||||
|
||||
function onFileCreated(err) {
|
||||
if (err.result !== global.DEFAULT_OK_RESPONSE.result) {
|
||||
logger.write(MODULE_NAME, "[ERROR] writeSnapshotFile -> onFileCreated -> err = " + err.stack);
|
||||
logger.write(MODULE_NAME, "[ERROR] writeSnapshotFile -> onFileCreated -> filePath = " + filePath);
|
||||
logger.write(MODULE_NAME, "[ERROR] writeSnapshotFile -> onFileCreated -> market = " + market.baseAsset + "_" + market.quotedAsset);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] writeSnapshotFile -> err = " + err.stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
exports.newTradingEngine = function newTradingEngine(bot, logger) {
|
||||
/*
|
||||
We call the Trading Engine to the data structure that is needed in order to exevute the
|
||||
trading protocol with the specific rules defined at the Trading System.
|
||||
*/
|
||||
const MODULE_NAME = 'Trading Engine'
|
||||
let thisObject = {
|
||||
mantain: mantain,
|
||||
reset: reset,
|
||||
getNodeById: getNodeById,
|
||||
cloneValues: cloneValues,
|
||||
setCurrentCandle: setCurrentCandle,
|
||||
setCurrentCycle: setCurrentCycle,
|
||||
initializeNode: initializeNode,
|
||||
initialize: initialize,
|
||||
finalize: finalize
|
||||
}
|
||||
|
||||
let tradingEngine
|
||||
let sessionParameters
|
||||
let nodesMap = new Map()
|
||||
|
||||
return thisObject
|
||||
|
||||
function initialize() {
|
||||
|
||||
tradingEngine = bot.simulationState.tradingEngine
|
||||
sessionParameters = bot.TRADING_SESSION.tradingParameters
|
||||
|
||||
initializeNodeMap(tradingEngine)
|
||||
|
||||
if (bot.FIRST_EXECUTION === true) {
|
||||
/*
|
||||
Here we will go through all the nodes in the Trading Engine hiriarchy and
|
||||
apply the initial value to the value property when needed
|
||||
*/
|
||||
initializeNode(tradingEngine)
|
||||
}
|
||||
}
|
||||
|
||||
function finalize() {
|
||||
tradingEngine = undefined
|
||||
sessionParameters = undefined
|
||||
nodesMap = undefined
|
||||
}
|
||||
|
||||
function mantain() {
|
||||
}
|
||||
|
||||
function reset() {
|
||||
|
||||
}
|
||||
|
||||
function getNodeById(NodeId) {
|
||||
return nodesMap.get(NodeId)
|
||||
}
|
||||
|
||||
function setCurrentCandle(candle) {
|
||||
tradingEngine.current.episode.candle.begin.value = candle.begin
|
||||
tradingEngine.current.episode.candle.end.value = candle.end
|
||||
tradingEngine.current.episode.candle.open.value = candle.open
|
||||
tradingEngine.current.episode.candle.close.value = candle.close
|
||||
tradingEngine.current.episode.candle.min.value = candle.min
|
||||
tradingEngine.current.episode.candle.max.value = candle.max
|
||||
}
|
||||
|
||||
function setCurrentCycle(cycle) {
|
||||
tradingEngine.current.episode.cycle.value = cycle
|
||||
tradingEngine.current.episode.cycle.lastBegin.value = tradingEngine.current.episode.cycle.begin.value
|
||||
tradingEngine.current.episode.cycle.lastEnd.value = tradingEngine.current.episode.cycle.end.value
|
||||
switch (cycle) {
|
||||
case 'First': {
|
||||
tradingEngine.current.episode.cycle.begin.value =
|
||||
tradingEngine.current.episode.candle.begin.value +
|
||||
sessionParameters.timeFrame.config.value
|
||||
tradingEngine.current.episode.cycle.end.value =
|
||||
tradingEngine.current.episode.candle.begin.value +
|
||||
sessionParameters.timeFrame.config.value +
|
||||
sessionParameters.timeFrame.config.value / 2 - 1
|
||||
break
|
||||
}
|
||||
case 'Second': {
|
||||
tradingEngine.current.episode.cycle.begin.value =
|
||||
tradingEngine.current.episode.candle.begin.value +
|
||||
sessionParameters.timeFrame.config.value +
|
||||
sessionParameters.timeFrame.config.value / 2
|
||||
tradingEngine.current.episode.cycle.end.value =
|
||||
tradingEngine.current.episode.candle.begin.value +
|
||||
sessionParameters.timeFrame.config.value +
|
||||
sessionParameters.timeFrame.config.value * 4 / 4 - 1
|
||||
break
|
||||
}
|
||||
}
|
||||
/*
|
||||
We can not have the last begin or last end to be zero, because that would prevent
|
||||
objects starting with lastBegin in zero to being saved on file. For that reason
|
||||
we will do this:
|
||||
*/
|
||||
if (tradingEngine.current.episode.cycle.lastBegin.value === 0) {
|
||||
tradingEngine.current.episode.cycle.lastBegin.value = tradingEngine.current.episode.cycle.begin.value
|
||||
}
|
||||
if (tradingEngine.current.episode.cycle.lastEnd.value === 0) {
|
||||
tradingEngine.current.episode.cycle.lastEnd.value = tradingEngine.current.episode.cycle.end.value
|
||||
}
|
||||
}
|
||||
|
||||
function cloneValues(originNode, destinationNode) {
|
||||
if (originNode === undefined) { return }
|
||||
if (destinationNode === undefined) { return }
|
||||
|
||||
/* Here copy the node value */
|
||||
destinationNode.value = originNode.value
|
||||
|
||||
/* Now we go down through all the children of the origin node, assuming the destination node has the same children structure*/
|
||||
let nodeDefinition = global.APP_SCHEMA_MAP.get(originNode.type)
|
||||
if (nodeDefinition === undefined) { return }
|
||||
|
||||
if (nodeDefinition.properties !== undefined) {
|
||||
let previousPropertyName // Since there are cases where there are many properties with the same name,because they can hold nodes of different types but only one at the time, we have to avoind counting each property of those as individual children.
|
||||
for (let i = 0; i < nodeDefinition.properties.length; i++) {
|
||||
let property = nodeDefinition.properties[i]
|
||||
|
||||
switch (property.type) {
|
||||
case 'node': {
|
||||
if (property.name !== previousPropertyName) {
|
||||
if (originNode[property.name] !== undefined && destinationNode[property.name] !== undefined) {
|
||||
cloneValues(originNode[property.name], destinationNode[property.name])
|
||||
}
|
||||
previousPropertyName = property.name
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'array': {
|
||||
if (originNode[property.name] !== undefined && destinationNode[property.name] !== undefined) {
|
||||
let originNodePropertyArray = originNode[property.name]
|
||||
let destinationNodePropertyArray = destinationNode[property.name]
|
||||
for (let m = 0; m < originNodePropertyArray.length; m++) {
|
||||
cloneValues(originNodePropertyArray[m], destinationNodePropertyArray[m])
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function initializeNodeMap(node) {
|
||||
if (node === undefined) { return }
|
||||
/* Add to Node Map */
|
||||
nodesMap.set(node.id, node)
|
||||
|
||||
/* Now we go down through all this node children */
|
||||
let nodeDefinition = global.APP_SCHEMA_MAP.get(node.type)
|
||||
if (nodeDefinition === undefined) { return }
|
||||
|
||||
if (nodeDefinition.properties !== undefined) {
|
||||
let previousPropertyName // Since there are cases where there are many properties with the same name,because they can hold nodes of different types but only one at the time, we have to avoind counting each property of those as individual children.
|
||||
for (let i = 0; i < nodeDefinition.properties.length; i++) {
|
||||
let property = nodeDefinition.properties[i]
|
||||
|
||||
switch (property.type) {
|
||||
case 'node': {
|
||||
if (property.name !== previousPropertyName) {
|
||||
if (node[property.name] !== undefined) {
|
||||
initializeNodeMap(node[property.name])
|
||||
}
|
||||
previousPropertyName = property.name
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'array': {
|
||||
if (node[property.name] !== undefined) {
|
||||
let nodePropertyArray = node[property.name]
|
||||
for (let m = 0; m < nodePropertyArray.length; m++) {
|
||||
initializeNodeMap(nodePropertyArray[m])
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function initializeNode(node) {
|
||||
if (node === undefined) { return }
|
||||
|
||||
/* Here we initialize the node value */
|
||||
if (node.config !== undefined) {
|
||||
if (node.config.initialValue !== undefined) {
|
||||
node.value = node.config.initialValue
|
||||
}
|
||||
}
|
||||
|
||||
/* Now we go down through all this node children */
|
||||
let nodeDefinition = global.APP_SCHEMA_MAP.get(node.type)
|
||||
if (nodeDefinition === undefined) { return }
|
||||
|
||||
if (nodeDefinition.properties !== undefined) {
|
||||
let previousPropertyName // Since there are cases where there are many properties with the same name,because they can hold nodes of different types but only one at the time, we have to avoind counting each property of those as individual children.
|
||||
for (let i = 0; i < nodeDefinition.properties.length; i++) {
|
||||
let property = nodeDefinition.properties[i]
|
||||
|
||||
switch (property.type) {
|
||||
case 'node': {
|
||||
if (property.name !== previousPropertyName) {
|
||||
if (node[property.name] !== undefined) {
|
||||
initializeNode(node[property.name])
|
||||
}
|
||||
previousPropertyName = property.name
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'array': {
|
||||
if (node[property.name] !== undefined) {
|
||||
let nodePropertyArray = node[property.name]
|
||||
for (let m = 0; m < nodePropertyArray.length; m++) {
|
||||
initializeNode(nodePropertyArray[m])
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,320 @@
|
||||
exports.newTradingEpisode = function newTradingEpisode(bot, logger, tradingEngineModule) {
|
||||
/*
|
||||
This module packages all functions related to Episodes.
|
||||
*/
|
||||
const MODULE_NAME = 'Trading Episode'
|
||||
let thisObject = {
|
||||
mantain: mantain,
|
||||
reset: reset,
|
||||
openEpisode: openEpisode,
|
||||
updateExitType: updateExitType,
|
||||
closeEpisode: closeEpisode,
|
||||
cycleBasedStatistics: cycleBasedStatistics,
|
||||
initialize: initialize,
|
||||
finalize: finalize
|
||||
}
|
||||
|
||||
let tradingEngine
|
||||
let tradingSystem
|
||||
let sessionParameters
|
||||
|
||||
return thisObject
|
||||
|
||||
function initialize() {
|
||||
tradingSystem = bot.simulationState.tradingSystem
|
||||
tradingEngine = bot.simulationState.tradingEngine
|
||||
sessionParameters = bot.TRADING_SESSION.tradingParameters
|
||||
}
|
||||
|
||||
function finalize() {
|
||||
tradingEngine = undefined
|
||||
tradingSystem = undefined
|
||||
sessionParameters = undefined
|
||||
}
|
||||
|
||||
function mantain() {
|
||||
updateCounters()
|
||||
updateStatistics()
|
||||
updateEnds()
|
||||
updateDistanceToEventsCounters()
|
||||
}
|
||||
|
||||
function reset() {
|
||||
resetTradingEngineDataStructure()
|
||||
}
|
||||
|
||||
function openEpisode() {
|
||||
/*
|
||||
This function is called each time the simulation starts. That does not mean the Episode
|
||||
must be opened there, since it might happen that it is looping at the end of the market
|
||||
or the task / session was restarted. We will only execute if the episode was never opened before.
|
||||
*/
|
||||
if (tradingEngine.current.episode.begin.value !== tradingEngine.current.episode.begin.config.initialValue) { return }
|
||||
|
||||
/* Starting begin and end */
|
||||
tradingEngine.current.episode.begin.value = tradingEngine.current.episode.candle.begin.value
|
||||
tradingEngine.current.episode.end.value = tradingEngine.current.episode.candle.end.value
|
||||
|
||||
/* Getting the begin Balance from the session configuration */
|
||||
tradingEngine.current.episode.episodeBaseAsset.beginBalance.value = sessionParameters.sessionBaseAsset.config.initialBalance
|
||||
tradingEngine.current.episode.episodeQuotedAsset.beginBalance.value = sessionParameters.sessionQuotedAsset.config.initialBalance
|
||||
|
||||
/* The current balance is also the begin balance, that is how this starts. */
|
||||
tradingEngine.current.episode.episodeBaseAsset.balance.value = sessionParameters.sessionBaseAsset.config.initialBalance
|
||||
tradingEngine.current.episode.episodeQuotedAsset.balance.value = sessionParameters.sessionQuotedAsset.config.initialBalance
|
||||
|
||||
/* Recording the opening at the Trading Engine Data Structure */
|
||||
tradingEngine.current.episode.status.value = 'Open'
|
||||
tradingEngine.current.episode.serialNumber.value = 1
|
||||
tradingEngine.current.episode.identifier.value = global.UNIQUE_ID()
|
||||
tradingEngine.current.episode.beginRate.value = tradingEngine.current.episode.candle.close.value
|
||||
}
|
||||
|
||||
function updateExitType(exitType) {
|
||||
tradingEngine.current.episode.exitType.value = exitType
|
||||
}
|
||||
|
||||
function closeEpisode() {
|
||||
tradingEngine.current.episode.status.value = 'Closed'
|
||||
tradingEngine.current.episode.end.value = tradingEngine.current.episode.candle.end.value
|
||||
tradingEngine.current.episode.endRate.value = tradingEngine.current.episode.candle.close.value
|
||||
tradingEngine.current.episode.episodeBaseAsset.endBalance.value = tradingEngine.current.episode.episodeBaseAsset.balance.value
|
||||
tradingEngine.current.episode.episodeQuotedAsset.endBalance.value = tradingEngine.current.episode.episodeQuotedAsset.balance.value
|
||||
}
|
||||
|
||||
function resetEpisode() {
|
||||
tradingEngineModule.initializeNode(tradingEngine.current.episode)
|
||||
}
|
||||
|
||||
function updateEnds() {
|
||||
if (tradingEngine.current.episode.status.value === 'Open') {
|
||||
tradingEngine.current.episode.end.value = tradingEngine.current.episode.end.value + sessionParameters.timeFrame.config.value
|
||||
tradingEngine.current.episode.endRate.value = tradingEngine.current.episode.candle.close.value
|
||||
tradingEngine.current.episode.episodeBaseAsset.endBalance.value = tradingEngine.current.episode.episodeBaseAsset.balance.value
|
||||
tradingEngine.current.episode.episodeQuotedAsset.endBalance.value = tradingEngine.current.episode.episodeQuotedAsset.balance.value
|
||||
}
|
||||
}
|
||||
|
||||
function resetTradingEngineDataStructure() {
|
||||
if (tradingEngine.current.episode.status.value === 'Closed') {
|
||||
resetEpisode()
|
||||
}
|
||||
}
|
||||
|
||||
function updateCounters() {
|
||||
if (tradingEngine.current.episode.status.value === 'Open') {
|
||||
tradingEngine.current.episode.episodeCounters.periods.value++
|
||||
}
|
||||
}
|
||||
|
||||
function updateStatistics() {
|
||||
|
||||
/* Daus Calculation */
|
||||
tradingEngine.current.episode.episodeStatistics.days.value =
|
||||
tradingEngine.current.episode.episodeCounters.periods.value *
|
||||
sessionParameters.timeFrame.config.value /
|
||||
global.ONE_DAY_IN_MILISECONDS
|
||||
}
|
||||
|
||||
function cycleBasedStatistics() {
|
||||
|
||||
calculateAssetsStatistics()
|
||||
calculateEpisodeStatistics()
|
||||
|
||||
function calculateAssetsStatistics() {
|
||||
/* Updating Profit Loss */
|
||||
tradingEngine.current.episode.episodeBaseAsset.profitLoss.value =
|
||||
tradingEngine.current.episode.episodeBaseAsset.balance.value -
|
||||
sessionParameters.sessionBaseAsset.config.initialBalance
|
||||
|
||||
tradingEngine.current.episode.episodeQuotedAsset.profitLoss.value =
|
||||
tradingEngine.current.episode.episodeQuotedAsset.balance.value -
|
||||
sessionParameters.sessionQuotedAsset.config.initialBalance
|
||||
|
||||
tradingEngine.current.episode.episodeBaseAsset.profitLoss.value = global.PRECISE(tradingEngine.current.episode.episodeBaseAsset.profitLoss.value, 10)
|
||||
tradingEngine.current.episode.episodeQuotedAsset.profitLoss.value = global.PRECISE(tradingEngine.current.episode.episodeQuotedAsset.profitLoss.value, 10)
|
||||
|
||||
/*
|
||||
Updating ROI
|
||||
|
||||
https://www.investopedia.com/articles/basics/10/guide-to-calculating-roi.asp
|
||||
*/
|
||||
tradingEngine.current.episode.episodeBaseAsset.ROI.value =
|
||||
tradingEngine.current.episode.episodeBaseAsset.profitLoss.value /
|
||||
tradingEngine.current.episode.episodeBaseAsset.beginBalance.value * 100
|
||||
|
||||
tradingEngine.current.episode.episodeQuotedAsset.ROI.value =
|
||||
tradingEngine.current.episode.episodeQuotedAsset.profitLoss.value /
|
||||
tradingEngine.current.episode.episodeQuotedAsset.beginBalance.value * 100
|
||||
|
||||
tradingEngine.current.episode.episodeBaseAsset.ROI.value = global.PRECISE(tradingEngine.current.episode.episodeBaseAsset.ROI.value, 10)
|
||||
tradingEngine.current.episode.episodeQuotedAsset.ROI.value = global.PRECISE(tradingEngine.current.episode.episodeQuotedAsset.ROI.value, 10)
|
||||
|
||||
/* Updating Hit Ratio */
|
||||
if (tradingEngine.current.episode.episodeCounters.positions.value > 0) {
|
||||
tradingEngine.current.episode.episodeBaseAsset.hitRatio.value =
|
||||
tradingEngine.current.episode.episodeBaseAsset.hits.value /
|
||||
tradingEngine.current.episode.episodeCounters.positions.value
|
||||
|
||||
tradingEngine.current.episode.episodeQuotedAsset.hitRatio.value =
|
||||
tradingEngine.current.episode.episodeQuotedAsset.hits.value /
|
||||
tradingEngine.current.episode.episodeCounters.positions.value
|
||||
|
||||
tradingEngine.current.episode.episodeBaseAsset.hitRatio.value = global.PRECISE(tradingEngine.current.episode.episodeBaseAsset.hitRatio.value, 10)
|
||||
tradingEngine.current.episode.episodeQuotedAsset.hitRatio.value = global.PRECISE(tradingEngine.current.episode.episodeQuotedAsset.hitRatio.value, 10)
|
||||
}
|
||||
|
||||
/*
|
||||
Updating Annualized Rate Of Return
|
||||
|
||||
https://www.investopedia.com/terms/a/annualized-rate.asp
|
||||
*/
|
||||
tradingEngine.current.episode.episodeBaseAsset.annualizedRateOfReturn.value =
|
||||
Math.pow(
|
||||
(
|
||||
tradingEngine.current.episode.episodeBaseAsset.beginBalance.value +
|
||||
tradingEngine.current.episode.episodeBaseAsset.profitLoss.value
|
||||
) / tradingEngine.current.episode.episodeBaseAsset.beginBalance.value
|
||||
,
|
||||
(365 / tradingEngine.current.episode.episodeStatistics.days.value)
|
||||
) - 1
|
||||
|
||||
tradingEngine.current.episode.episodeQuotedAsset.annualizedRateOfReturn.value =
|
||||
Math.pow(
|
||||
(
|
||||
tradingEngine.current.episode.episodeQuotedAsset.beginBalance.value +
|
||||
tradingEngine.current.episode.episodeQuotedAsset.profitLoss.value
|
||||
) / tradingEngine.current.episode.episodeQuotedAsset.beginBalance.value
|
||||
,
|
||||
(365 / tradingEngine.current.episode.episodeStatistics.days.value)
|
||||
) - 1
|
||||
|
||||
tradingEngine.current.episode.episodeBaseAsset.annualizedRateOfReturn.value = global.PRECISE(tradingEngine.current.episode.episodeBaseAsset.annualizedRateOfReturn.value, 10)
|
||||
tradingEngine.current.episode.episodeQuotedAsset.annualizedRateOfReturn.value = global.PRECISE(tradingEngine.current.episode.episodeQuotedAsset.annualizedRateOfReturn.value, 10)
|
||||
|
||||
/* Updating Hit or Fail */
|
||||
if (tradingEngine.current.episode.episodeBaseAsset.profitLoss.value > 0) {
|
||||
tradingEngine.current.episode.episodeBaseAsset.hitFail.value = 'Hit'
|
||||
} else {
|
||||
tradingEngine.current.episode.episodeBaseAsset.hitFail.value = 'Fail'
|
||||
}
|
||||
if (tradingEngine.current.episode.episodeQuotedAsset.profitLoss.value > 0) {
|
||||
tradingEngine.current.episode.episodeQuotedAsset.hitFail.value = 'Hit'
|
||||
} else {
|
||||
tradingEngine.current.episode.episodeQuotedAsset.hitFail.value = 'Fail'
|
||||
}
|
||||
}
|
||||
|
||||
function calculateEpisodeStatistics() {
|
||||
/* Updating Profit Loss */
|
||||
tradingEngine.current.episode.episodeStatistics.profitLoss.value =
|
||||
tradingEngine.current.episode.episodeBaseAsset.profitLoss.value * tradingEngine.current.episode.candle.close.value +
|
||||
tradingEngine.current.episode.episodeQuotedAsset.profitLoss.value
|
||||
|
||||
tradingEngine.current.episode.episodeStatistics.profitLoss.value = global.PRECISE(tradingEngine.current.episode.episodeStatistics.profitLoss.value, 10)
|
||||
|
||||
/*
|
||||
Updating ROI
|
||||
|
||||
https://www.investopedia.com/articles/basics/10/guide-to-calculating-roi.asp
|
||||
*/
|
||||
tradingEngine.current.episode.episodeStatistics.ROI.value =
|
||||
(
|
||||
tradingEngine.current.episode.episodeBaseAsset.profitLoss.value * tradingEngine.current.episode.endRate.value +
|
||||
tradingEngine.current.episode.episodeQuotedAsset.profitLoss.value
|
||||
) / (
|
||||
tradingEngine.current.episode.episodeBaseAsset.beginBalance.value * tradingEngine.current.episode.beginRate.value +
|
||||
tradingEngine.current.episode.episodeQuotedAsset.beginBalance.value
|
||||
) * 100
|
||||
|
||||
tradingEngine.current.episode.episodeStatistics.ROI.value = global.PRECISE(tradingEngine.current.episode.episodeStatistics.ROI.value, 10)
|
||||
|
||||
/*
|
||||
Updating Annualized Rate Of Return
|
||||
|
||||
https://www.investopedia.com/terms/a/annualized-rate.asp
|
||||
*/
|
||||
tradingEngine.current.episode.episodeStatistics.annualizedRateOfReturn.value =
|
||||
Math.pow(
|
||||
(
|
||||
tradingEngine.current.episode.episodeBaseAsset.beginBalance.value * tradingEngine.current.episode.beginRate.value +
|
||||
tradingEngine.current.episode.episodeQuotedAsset.beginBalance.value +
|
||||
tradingEngine.current.episode.episodeBaseAsset.profitLoss.value +
|
||||
tradingEngine.current.episode.episodeQuotedAsset.profitLoss.value
|
||||
) /
|
||||
(
|
||||
tradingEngine.current.episode.episodeBaseAsset.beginBalance.value * tradingEngine.current.episode.beginRate.value +
|
||||
tradingEngine.current.episode.episodeQuotedAsset.beginBalance.value
|
||||
)
|
||||
,
|
||||
(365 / tradingEngine.current.episode.episodeStatistics.days.value)
|
||||
) - 1
|
||||
|
||||
tradingEngine.current.episode.episodeStatistics.annualizedRateOfReturn.value = global.PRECISE(tradingEngine.current.episode.episodeStatistics.annualizedRateOfReturn.value, 10)
|
||||
|
||||
/* Updating Hit or Fail */
|
||||
if (tradingEngine.current.episode.episodeStatistics.profitLoss.value > 0) {
|
||||
tradingEngine.current.episode.episodeStatistics.hitFail.value = 'Hit'
|
||||
} else {
|
||||
tradingEngine.current.episode.episodeStatistics.hitFail.value = 'Fail'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateDistanceToEventsCounters() {
|
||||
/* Keeping Distance Counters Up-to-date while avoinding counting before the first event happens. */
|
||||
if (
|
||||
tradingEngine.current.episode.distanceToEvent.triggerOn.value > 0
|
||||
) {
|
||||
tradingEngine.current.episode.distanceToEvent.triggerOn.value++
|
||||
}
|
||||
|
||||
if (
|
||||
tradingEngine.current.episode.distanceToEvent.triggerOff.value > 0
|
||||
) {
|
||||
tradingEngine.current.episode.distanceToEvent.triggerOff.value++
|
||||
}
|
||||
|
||||
if (
|
||||
tradingEngine.current.episode.distanceToEvent.takePosition.value > 0
|
||||
) {
|
||||
tradingEngine.current.episode.distanceToEvent.takePosition.value++
|
||||
}
|
||||
|
||||
if (
|
||||
tradingEngine.current.episode.distanceToEvent.closePosition.value > 0
|
||||
) {
|
||||
tradingEngine.current.episode.distanceToEvent.closePosition.value++
|
||||
}
|
||||
|
||||
if (
|
||||
tradingEngine.current.episode.distanceToEvent.nextPhase.value > 0
|
||||
) {
|
||||
tradingEngine.current.episode.distanceToEvent.nextPhase.value++
|
||||
}
|
||||
|
||||
if (
|
||||
tradingEngine.current.episode.distanceToEvent.moveToPhase.value > 0
|
||||
) {
|
||||
tradingEngine.current.episode.distanceToEvent.moveToPhase.value++
|
||||
}
|
||||
|
||||
if (
|
||||
tradingEngine.current.episode.distanceToEvent.createOrder.value > 0
|
||||
) {
|
||||
tradingEngine.current.episode.distanceToEvent.createOrder.value++
|
||||
}
|
||||
|
||||
if (
|
||||
tradingEngine.current.episode.distanceToEvent.cancelOrder.value > 0
|
||||
) {
|
||||
tradingEngine.current.episode.distanceToEvent.cancelOrder.value++
|
||||
}
|
||||
|
||||
if (
|
||||
tradingEngine.current.episode.distanceToEvent.closeOrder.value > 0
|
||||
) {
|
||||
tradingEngine.current.episode.distanceToEvent.closeOrder.value++
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
exports.newTradingExecution = function newTradingExecution(bot, logger, tradingEngineModule) {
|
||||
/*
|
||||
The Trading Execution modules manages the execution nodes and execution algoritms.
|
||||
*/
|
||||
const MODULE_NAME = 'Trading Execution'
|
||||
|
||||
let thisObject = {
|
||||
mantain: mantain,
|
||||
reset: reset,
|
||||
runExecution: runExecution,
|
||||
initialize: initialize,
|
||||
finalize: finalize
|
||||
}
|
||||
|
||||
let tradingEngine
|
||||
let tradingSystem
|
||||
let sessionParameters
|
||||
|
||||
const TRADING_ORDERS_MODULE = require('./TradingOrders.js')
|
||||
let tradingOrdersModule = TRADING_ORDERS_MODULE.newTradingOrders(bot, logger, tradingEngineModule)
|
||||
|
||||
return thisObject
|
||||
|
||||
function initialize() {
|
||||
tradingSystem = bot.simulationState.tradingSystem
|
||||
tradingEngine = bot.simulationState.tradingEngine
|
||||
sessionParameters = bot.TRADING_SESSION.tradingParameters
|
||||
|
||||
tradingOrdersModule.initialize()
|
||||
}
|
||||
|
||||
function finalize() {
|
||||
tradingSystem = undefined
|
||||
tradingEngine = undefined
|
||||
sessionParameters = undefined
|
||||
|
||||
tradingOrdersModule.finalize()
|
||||
tradingOrdersModule = undefined
|
||||
}
|
||||
|
||||
function mantain() {
|
||||
tradingOrdersModule.mantain()
|
||||
}
|
||||
|
||||
function reset() {
|
||||
tradingOrdersModule.reset()
|
||||
}
|
||||
|
||||
async function runExecution(
|
||||
executionNode,
|
||||
tradingEngineStage
|
||||
) {
|
||||
try {
|
||||
/* Trading Engine Stage Validations */
|
||||
if (tradingEngineStage.stageBaseAsset === undefined) { badDefinitionUnhandledException(undefined, 'stageBaseAsset === undefined', tradingEngineStage) }
|
||||
if (tradingEngineStage.stageBaseAsset.targetSize === undefined) { badDefinitionUnhandledException(undefined, 'stageBaseAsset.targetSize === undefined', tradingEngineStage) }
|
||||
if (tradingEngineStage.stageBaseAsset.sizePlaced === undefined) { badDefinitionUnhandledException(undefined, 'stageBaseAsset.sizePlaced === undefined', tradingEngineStage) }
|
||||
if (tradingEngineStage.stageBaseAsset.sizeFilled === undefined) { badDefinitionUnhandledException(undefined, 'stageBaseAsset.sizeFilled === undefined', tradingEngineStage) }
|
||||
if (tradingEngineStage.stageBaseAsset.feesPaid === undefined) { badDefinitionUnhandledException(undefined, 'stageBaseAsset.feesPaid === undefined', tradingEngineStage) }
|
||||
if (tradingEngineStage.stageQuotedAsset === undefined) { badDefinitionUnhandledException(undefined, 'stageQuotedAsset === undefined', tradingEngineStage) }
|
||||
if (tradingEngineStage.stageQuotedAsset.targetSize === undefined) { badDefinitionUnhandledException(undefined, 'stageQuotedAsset.targetSize === undefined', tradingEngineStage) }
|
||||
if (tradingEngineStage.stageQuotedAsset.sizePlaced === undefined) { badDefinitionUnhandledException(undefined, 'stageQuotedAsset.sizePlaced === undefined', tradingEngineStage) }
|
||||
if (tradingEngineStage.stageQuotedAsset.sizeFilled === undefined) { badDefinitionUnhandledException(undefined, 'stageQuotedAsset.sizeFilled === undefined', tradingEngineStage) }
|
||||
if (tradingEngineStage.stageQuotedAsset.feesPaid === undefined) { badDefinitionUnhandledException(undefined, 'stageQuotedAsset.feesPaid === undefined', tradingEngineStage) }
|
||||
|
||||
await checkExecutionAlgorithms(executionNode)
|
||||
|
||||
async function checkExecutionAlgorithms(executionNode) {
|
||||
for (let i = 0; i < executionNode.executionAlgorithms.length; i++) {
|
||||
let executionAlgorithm = executionNode.executionAlgorithms[i]
|
||||
await tradingOrdersModule.checkOrders(tradingEngineStage, executionAlgorithm.marketBuyOrders, executionAlgorithm, executionNode)
|
||||
await tradingOrdersModule.checkOrders(tradingEngineStage, executionAlgorithm.marketSellOrders, executionAlgorithm, executionNode)
|
||||
await tradingOrdersModule.checkOrders(tradingEngineStage, executionAlgorithm.limitBuyOrders, executionAlgorithm, executionNode)
|
||||
await tradingOrdersModule.checkOrders(tradingEngineStage, executionAlgorithm.limitSellOrders, executionAlgorithm, executionNode)
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
tradingSystem.errors.push([executionNode.id, err.message])
|
||||
|
||||
if (typeof err === 'string' || err instanceof String) {
|
||||
logger.write(MODULE_NAME, '[ERROR] runExecution -> err = ' + err)
|
||||
}
|
||||
if (err.stack !== undefined) {
|
||||
logger.write(MODULE_NAME, '[ERROR] runExecution -> err = ' + err.stack)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function badDefinitionUnhandledException(err, message, node) {
|
||||
tradingSystem.errors.push([node.id, message])
|
||||
|
||||
logger.write(MODULE_NAME, "[ERROR] -> " + message);
|
||||
logger.write(MODULE_NAME, "[ERROR] -> node.name = " + node.name);
|
||||
logger.write(MODULE_NAME, "[ERROR] -> node.type = " + node.type);
|
||||
logger.write(MODULE_NAME, "[ERROR] -> node.config = " + JSON.stringify(node.config));
|
||||
if (err !== undefined) {
|
||||
logger.write(MODULE_NAME, "[ERROR] -> err.stack = " + err.stack);
|
||||
}
|
||||
throw 'Please fix the problem and try again.'
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,233 @@
|
||||
exports.newTradingOutput = function newTradingOutput(bot, logger, tradingEngineModule, UTILITIES, FILE_STORAGE) {
|
||||
/*
|
||||
This module will load if necesary all the data outputs so that they can be appended with new
|
||||
records if needed. After running the simulation, it will save all the data outputs.
|
||||
*/
|
||||
const MODULE_NAME = 'Trading Output'
|
||||
|
||||
let thisObject = {
|
||||
initialize: initialize,
|
||||
finalize: finalize,
|
||||
start: start
|
||||
}
|
||||
|
||||
let utilities = UTILITIES.newCloudUtilities(logger)
|
||||
let fileStorage = FILE_STORAGE.newFileStorage(logger)
|
||||
|
||||
return thisObject
|
||||
|
||||
function finalize() {
|
||||
thisObject = undefined
|
||||
utilities = undefined
|
||||
fileStorage = undefined
|
||||
commons = undefined
|
||||
}
|
||||
|
||||
function initialize() {
|
||||
|
||||
}
|
||||
|
||||
async function start(chart, timeFrame, timeFrameLabel, tradingProcessDate) {
|
||||
try {
|
||||
|
||||
if (timeFrame > global.dailyFilePeriods[0][0]) {
|
||||
bot.processingDailyFiles = false
|
||||
} else {
|
||||
bot.processingDailyFiles = true
|
||||
}
|
||||
|
||||
/* Preparing everything for the Simulation */
|
||||
const TRADING_SIMULATION = require('./TradingSimulation.js')
|
||||
let tradingSimulation = TRADING_SIMULATION.newTradingSimulation(bot, logger, tradingEngineModule, UTILITIES)
|
||||
|
||||
let outputDatasets = global.NODE_BRANCH_TO_ARRAY(bot.processNode.referenceParent.processOutput, 'Output Dataset')
|
||||
let outputDatasetsMap = new Map()
|
||||
|
||||
if (bot.processingDailyFiles === true) {
|
||||
/*
|
||||
For Daily Files, we are going to initialize the outputs at the first execution
|
||||
if we are not resuming, but also at every following execution except when
|
||||
we are at the head of the market, because at the head of the market we need to
|
||||
append more data to the same output files that already exists.
|
||||
*/
|
||||
if (bot.FIRST_EXECUTION === true && bot.RESUME === false) {
|
||||
await initializeOutputs()
|
||||
} else {
|
||||
if (bot.simulationState.tradingEngine.current.episode.headOfTheMarket.value === true) {
|
||||
await readFiles()
|
||||
} else {
|
||||
await initializeOutputs()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/*
|
||||
Form Market Files, we are only going to initialize the outputs at the first run
|
||||
and if we are not resuming execution. Subsequent runs means that we are at the
|
||||
head of the market. Remember that backtests finishes the session once the reach
|
||||
the final datetime.
|
||||
*/
|
||||
if (bot.FIRST_EXECUTION === true && bot.RESUME === false) {
|
||||
await initializeOutputs()
|
||||
} else {
|
||||
await readFiles()
|
||||
}
|
||||
}
|
||||
|
||||
await tradingSimulation.runSimulation(
|
||||
chart,
|
||||
outputDatasetsMap,
|
||||
writeFiles
|
||||
)
|
||||
|
||||
tradingSimulation.finalize()
|
||||
return
|
||||
|
||||
async function initializeOutputs() {
|
||||
if (bot.processingDailyFiles) {
|
||||
await initializeDailyFiles()
|
||||
} else {
|
||||
await initializeMarketFiles()
|
||||
}
|
||||
}
|
||||
|
||||
async function initializeDailyFiles() {
|
||||
for (let i = 0; i < outputDatasets.length; i++) {
|
||||
let outputDatasetNode = outputDatasets[i]
|
||||
let dataset = outputDatasetNode.referenceParent
|
||||
|
||||
if (dataset.config.type === 'Daily Files') {
|
||||
outputDatasetsMap.set(dataset.parentNode.config.codeName, [])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function initializeMarketFiles() {
|
||||
for (let i = 0; i < outputDatasets.length; i++) {
|
||||
let outputDatasetNode = outputDatasets[i]
|
||||
let dataset = outputDatasetNode.referenceParent
|
||||
|
||||
if (dataset.config.type === 'Market Files') {
|
||||
outputDatasetsMap.set(dataset.parentNode.config.codeName, [])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function readFiles() {
|
||||
/*
|
||||
This bot have an output of files that it generates. At every call to the bot, it needs to read the previously generated
|
||||
files in order to later append more information after the execution is over. Here in this function we are going to
|
||||
read those output files and get them ready for appending content during the simulation.
|
||||
*/
|
||||
if (bot.processingDailyFiles) {
|
||||
await readDailyFiles()
|
||||
} else {
|
||||
await readMarketFiles()
|
||||
}
|
||||
}
|
||||
|
||||
async function readMarketFiles() {
|
||||
for (let i = 0; i < outputDatasets.length; i++) {
|
||||
let outputDatasetNode = outputDatasets[i]
|
||||
let dataset = outputDatasetNode.referenceParent
|
||||
|
||||
if (dataset.config.type === 'Market Files') {
|
||||
|
||||
let fileName = 'Data.json'
|
||||
let filePath = bot.filePathRoot + '/Output/' + bot.TRADING_SESSION.folderName + '/' + dataset.parentNode.config.codeName + '/' + dataset.config.codeName + '/' + timeFrameLabel
|
||||
|
||||
await readOutputFile(fileName, filePath, dataset.parentNode.config.codeName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function readDailyFiles() {
|
||||
for (let i = 0; i < outputDatasets.length; i++) {
|
||||
let outputDatasetNode = outputDatasets[i]
|
||||
let dataset = outputDatasetNode.referenceParent
|
||||
|
||||
if (dataset.config.type === 'Daily Files') {
|
||||
|
||||
let dateForPath = tradingProcessDate.getUTCFullYear() + '/' + utilities.pad(tradingProcessDate.getUTCMonth() + 1, 2) + '/' + utilities.pad(tradingProcessDate.getUTCDate(), 2);
|
||||
let fileName = 'Data.json'
|
||||
let filePath = bot.filePathRoot + '/Output/' + bot.TRADING_SESSION.folderName + '/' + dataset.parentNode.config.codeName + '/' + dataset.config.codeName + '/' + timeFrameLabel + "/" + dateForPath
|
||||
|
||||
await readOutputFile(fileName, filePath, dataset.parentNode.config.codeName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function readOutputFile(fileName, filePath, productName) {
|
||||
filePath += '/' + fileName
|
||||
|
||||
let response = await fileStorage.asyncGetTextFile(filePath, true)
|
||||
|
||||
if (response.err.message === 'File does not exist.') {
|
||||
outputDatasetsMap.set(productName, [])
|
||||
return
|
||||
}
|
||||
if (response.err.result !== global.DEFAULT_OK_RESPONSE.result) {
|
||||
throw(response.err)
|
||||
}
|
||||
outputDatasetsMap.set(productName, JSON.parse(response.text))
|
||||
}
|
||||
|
||||
async function writeFiles() {
|
||||
/*
|
||||
The output of files which were appended with information during the simulation execution, now needs to be saved.
|
||||
*/
|
||||
if (bot.processingDailyFiles) {
|
||||
await writeDailyFiles()
|
||||
} else {
|
||||
await writeMarketFiles()
|
||||
}
|
||||
}
|
||||
|
||||
async function writeMarketFiles() {
|
||||
for (let i = 0; i < outputDatasets.length; i++) {
|
||||
let outputDatasetNode = outputDatasets[i]
|
||||
let dataset = outputDatasetNode.referenceParent
|
||||
|
||||
if (dataset.config.type === 'Market Files') {
|
||||
|
||||
let fileName = 'Data.json'
|
||||
let filePath = bot.filePathRoot + '/Output/' + bot.TRADING_SESSION.folderName + '/' + dataset.parentNode.config.codeName + '/' + dataset.config.codeName + '/' + timeFrameLabel
|
||||
|
||||
await writeOutputFile(fileName, filePath, dataset.parentNode.config.codeName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function writeDailyFiles() {
|
||||
for (let i = 0; i < outputDatasets.length; i++) {
|
||||
let outputDatasetNode = outputDatasets[i]
|
||||
let dataset = outputDatasetNode.referenceParent
|
||||
|
||||
if (dataset.config.type === 'Daily Files') {
|
||||
|
||||
let dateForPath = tradingProcessDate.getUTCFullYear() + '/' + utilities.pad(tradingProcessDate.getUTCMonth() + 1, 2) + '/' + utilities.pad(tradingProcessDate.getUTCDate(), 2);
|
||||
let fileName = 'Data.json'
|
||||
let filePath = bot.filePathRoot + '/Output/' + bot.TRADING_SESSION.folderName + '/' + dataset.parentNode.config.codeName + '/' + dataset.config.codeName + '/' + timeFrameLabel + "/" + dateForPath
|
||||
|
||||
await writeOutputFile(fileName, filePath, dataset.parentNode.config.codeName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function writeOutputFile(fileName, filePath, productName) {
|
||||
filePath += '/' + fileName
|
||||
let fileContent = JSON.stringify(outputDatasetsMap.get(productName))
|
||||
|
||||
let response = await fileStorage.asyncCreateTextFile(filePath, fileContent)
|
||||
|
||||
if (response.err.result !== global.DEFAULT_OK_RESPONSE.result) {
|
||||
throw(response.err)
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, '[ERROR] start -> err = ' + err.stack)
|
||||
throw(global.DEFAULT_FAIL_RESPONSE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,432 @@
|
||||
exports.newTradingPosition = function newTradingPosition(bot, logger, tradingEngineModule) {
|
||||
/*
|
||||
This module packages all functions related to Positions.
|
||||
*/
|
||||
const MODULE_NAME = 'Trading Position'
|
||||
let thisObject = {
|
||||
mantain: mantain,
|
||||
reset: reset,
|
||||
cycleBasedStatistics: cycleBasedStatistics,
|
||||
openPosition: openPosition,
|
||||
closingPosition: closingPosition,
|
||||
closePosition: closePosition,
|
||||
applyStopLossFormula: applyStopLossFormula,
|
||||
applyTakeProfitFormula: applyTakeProfitFormula,
|
||||
updateStopLoss: updateStopLoss,
|
||||
updateTakeProfit: updateTakeProfit,
|
||||
initialTargets: initialTargets,
|
||||
updateEnds: updateEnds,
|
||||
resetTradingEngineDataStructure: resetTradingEngineDataStructure,
|
||||
updateCounters: updateCounters,
|
||||
initialize: initialize,
|
||||
finalize: finalize
|
||||
}
|
||||
|
||||
let tradingEngine
|
||||
let tradingSystem
|
||||
let sessionParameters
|
||||
|
||||
return thisObject
|
||||
|
||||
function initialize() {
|
||||
tradingSystem = bot.simulationState.tradingSystem
|
||||
tradingEngine = bot.simulationState.tradingEngine
|
||||
sessionParameters = bot.TRADING_SESSION.tradingParameters
|
||||
}
|
||||
|
||||
function finalize() {
|
||||
tradingEngine = undefined
|
||||
tradingSystem = undefined
|
||||
sessionParameters = undefined
|
||||
}
|
||||
|
||||
function mantain() {
|
||||
updateCounters()
|
||||
updateEnds()
|
||||
}
|
||||
|
||||
function reset() {
|
||||
resetTradingEngineDataStructure()
|
||||
}
|
||||
|
||||
function openPosition(situationName) {
|
||||
|
||||
/* Starting begin and end */
|
||||
tradingEngine.current.position.begin.value = tradingEngine.current.episode.cycle.lastBegin.value
|
||||
tradingEngine.current.position.end.value = tradingEngine.current.episode.cycle.lastEnd.value
|
||||
|
||||
/* Recording the opening at the Trading Engine Data Structure */
|
||||
tradingEngine.current.position.status.value = 'Open'
|
||||
tradingEngine.current.position.serialNumber.value = tradingEngine.current.episode.episodeCounters.positions.value + 1
|
||||
tradingEngine.current.position.identifier.value = global.UNIQUE_ID()
|
||||
tradingEngine.current.position.beginRate.value = tradingEngine.current.episode.candle.close.value
|
||||
tradingEngine.current.position.positionBaseAsset.beginBalance.value = tradingEngine.current.episode.episodeBaseAsset.balance.value
|
||||
tradingEngine.current.position.positionQuotedAsset.beginBalance.value = tradingEngine.current.episode.episodeQuotedAsset.balance.value
|
||||
tradingEngine.current.position.situationName.value = situationName
|
||||
|
||||
/* Initializing Stop and Take Phase */
|
||||
tradingEngine.current.position.stopLoss.stopLossPhase.value = 1
|
||||
tradingEngine.current.position.takeProfit.takeProfitPhase.value = 1
|
||||
|
||||
/* Updating Episode Counters */
|
||||
tradingEngine.current.episode.episodeCounters.positions.value++
|
||||
|
||||
/* Inicializing this counter */
|
||||
tradingEngine.current.episode.distanceToEvent.takePosition.value = 1
|
||||
|
||||
/* Remember the balance we had before taking the position to later calculate profit or loss */
|
||||
tradingEngine.current.position.positionBaseAsset.beginBalance = tradingEngine.current.episode.episodeBaseAsset.balance.value
|
||||
tradingEngine.current.position.positionQuotedAsset.beginBalance = tradingEngine.current.episode.episodeQuotedAsset.balance.value
|
||||
}
|
||||
|
||||
function closingPosition(exitType) {
|
||||
tradingEngine.current.position.status.value = 'Closing'
|
||||
tradingEngine.current.position.exitType.value = exitType
|
||||
|
||||
/*
|
||||
By the time we figured out that the stop loss or take profit were hit, or
|
||||
for whatever reason the position needs to be closed, the take profit and
|
||||
stop loss values for the next candle have already been calculated. In order
|
||||
to avoid being plotted, we will put them in zero.
|
||||
*/
|
||||
tradingEngine.current.position.stopLoss.value = 0
|
||||
tradingEngine.current.position.takeProfit.value = 0
|
||||
}
|
||||
|
||||
function closePosition() {
|
||||
tradingEngine.current.position.status.value = 'Closed'
|
||||
tradingEngine.current.position.end.value = tradingEngine.current.episode.cycle.lastEnd.value
|
||||
tradingEngine.current.position.endRate.value = tradingEngine.current.episode.candle.close.value
|
||||
tradingEngine.current.position.positionBaseAsset.endBalance.value = tradingEngine.current.episode.episodeBaseAsset.balance.value
|
||||
tradingEngine.current.position.positionQuotedAsset.endBalance.value = tradingEngine.current.episode.episodeQuotedAsset.balance.value
|
||||
|
||||
/*
|
||||
Now that the position is closed, it is the right time to move this position from current to last at the Trading Engine data structure.
|
||||
*/
|
||||
tradingEngineModule.cloneValues(tradingEngine.current.position, tradingEngine.last.position)
|
||||
|
||||
cycleBasedStatistics()
|
||||
|
||||
/* Updating Hits & Fails */
|
||||
if (tradingEngine.current.position.positionBaseAsset.profitLoss.value > 0) {
|
||||
tradingEngine.current.episode.episodeBaseAsset.hits.value++
|
||||
}
|
||||
if (tradingEngine.current.position.positionBaseAsset.profitLoss.value < 0) {
|
||||
tradingEngine.current.episode.episodeBaseAsset.fails.value++
|
||||
}
|
||||
if (tradingEngine.current.position.positionQuotedAsset.profitLoss.value > 0) {
|
||||
tradingEngine.current.episode.episodeQuotedAsset.hits.value++
|
||||
}
|
||||
if (tradingEngine.current.position.positionQuotedAsset.profitLoss.value < 0) {
|
||||
tradingEngine.current.episode.episodeQuotedAsset.fails.value++
|
||||
}
|
||||
}
|
||||
|
||||
function applyStopLossFormula(formulas, formulaId) {
|
||||
updateStopLossTakeProfitFinalValue(tradingEngine.current.position.stopLoss)
|
||||
tradingEngine.current.position.stopLoss.value = formulas.get(formulaId)
|
||||
updateStopLossTakeProfitInitialValue(tradingEngine.current.position.stopLoss)
|
||||
updateStopLossTakeProfitBeginEnd(tradingEngine.current.position.stopLoss)
|
||||
}
|
||||
|
||||
function applyTakeProfitFormula(formulas, formulaId) {
|
||||
updateStopLossTakeProfitFinalValue(tradingEngine.current.position.takeProfit)
|
||||
tradingEngine.current.position.takeProfit.value = formulas.get(formulaId)
|
||||
updateStopLossTakeProfitInitialValue(tradingEngine.current.position.takeProfit)
|
||||
updateStopLossTakeProfitBeginEnd(tradingEngine.current.position.takeProfit)
|
||||
}
|
||||
|
||||
function updateStopLossTakeProfitInitialValue(node) {
|
||||
/*
|
||||
We store the first Stop Loss or Take Profit value in a separate node.
|
||||
To know if it is the first one we compare its current value to the initial value.
|
||||
*/
|
||||
if (node.initialValue.value === node.initialValue.config.initialValue) {
|
||||
node.initialValue.value = node.value
|
||||
}
|
||||
}
|
||||
|
||||
function updateStopLossTakeProfitFinalValue(node) {
|
||||
/*
|
||||
We set the final value just before calcualting the new value, since the new calculated
|
||||
value is going to be discarded if the position is closed after it is calculated, but
|
||||
during the same candle.
|
||||
*/
|
||||
node.finalValue.value = node.value
|
||||
}
|
||||
|
||||
function updateStopLossTakeProfitBeginEnd(node) {
|
||||
/*
|
||||
Both the Stop Loss and the Take Profit have their own Begin and End.
|
||||
Ther reason for this is because they represent targets to be checked
|
||||
at the next candle, so it does not apply that they share the begin and
|
||||
end of the position itself.
|
||||
*/
|
||||
node.begin.value =
|
||||
tradingEngine.current.episode.candle.begin.value +
|
||||
sessionParameters.timeFrame.config.value
|
||||
node.end.value =
|
||||
tradingEngine.current.episode.candle.end.value +
|
||||
sessionParameters.timeFrame.config.value
|
||||
}
|
||||
|
||||
function updateStopLoss(phase) {
|
||||
tradingEngine.current.position.stopLoss.stopLossPhase.value = phase
|
||||
}
|
||||
|
||||
function updateTakeProfit(phase) {
|
||||
tradingEngine.current.position.takeProfit.takeProfitPhase.value = phase
|
||||
}
|
||||
|
||||
function initialTargets(tradingSystemStageNode, tradingEngineStageNode) {
|
||||
|
||||
if (tradingSystemStageNode.initialTargets === undefined) {
|
||||
const message = 'Stage without Initial Targets node. Add one please.'
|
||||
badDefinitionUnhandledException(undefined, message, tradingSystemStageNode)
|
||||
}
|
||||
|
||||
setTargetRate()
|
||||
setTargetSize()
|
||||
|
||||
function setTargetRate() {
|
||||
if (tradingSystemStageNode.initialTargets.targetRate === undefined) {
|
||||
const message = 'Target Rate Node not Found. Fix this please.'
|
||||
badDefinitionUnhandledException(undefined, message, tradingSystemStageNode.initialTargets)
|
||||
}
|
||||
if (tradingSystemStageNode.initialTargets.targetRate.formula === undefined) {
|
||||
const message = 'Formula of Target Rate Node not Found. Fix this please.'
|
||||
badDefinitionUnhandledException(undefined, message, tradingSystemStageNode.initialTargets.targetRate)
|
||||
}
|
||||
|
||||
let value = tradingSystem.formulas.get(tradingSystemStageNode.initialTargets.targetRate.formula.id)
|
||||
if (value === undefined) {
|
||||
const message = 'Target Rate can not be undefined. Fix this please.'
|
||||
badDefinitionUnhandledException(undefined, message, tradingSystemStageNode.initialTargets.targetRate.formula)
|
||||
}
|
||||
if (isNaN(value)) {
|
||||
const message = 'Target Rate must be a number. Fix this please.'
|
||||
badDefinitionUnhandledException(undefined, message, tradingSystemStageNode.initialTargets.targetRate.formula)
|
||||
}
|
||||
|
||||
switch (tradingSystemStageNode.type) {
|
||||
case 'Open Stage': {
|
||||
tradingEngine.current.position.entryTargetRate.value = global.PRECISE(value, 10)
|
||||
break
|
||||
}
|
||||
case 'Close Stage': {
|
||||
tradingEngine.current.position.exitTargetRate.value = global.PRECISE(value, 10)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setTargetSize() {
|
||||
/* Basic Validation */
|
||||
if (
|
||||
tradingSystemStageNode.initialTargets.targetSizeInBaseAsset !== undefined &&
|
||||
tradingSystemStageNode.initialTargets.targetSizeInQuotedAsset !== undefined
|
||||
) {
|
||||
const message = 'Only Target Size In Base Asset or Target Size In Quoted Asset is allowed. Remove one of them please.'
|
||||
badDefinitionUnhandledException(undefined, message, tradingSystemStageNode)
|
||||
}
|
||||
|
||||
/* Position In Base Asset */
|
||||
if (tradingSystemStageNode.initialTargets.targetSizeInBaseAsset !== undefined) {
|
||||
if (tradingSystemStageNode.initialTargets.targetSizeInBaseAsset.formula !== undefined) {
|
||||
let value = tradingSystem.formulas.get(tradingSystemStageNode.initialTargets.targetSizeInBaseAsset.formula.id)
|
||||
if (value === undefined) {
|
||||
const message = 'Target Size In Base Asset cannot be undefined. Fix this please.'
|
||||
badDefinitionUnhandledException(undefined, message, tradingSystemStageNode.initialTargets.targetSizeInBaseAsset.formula)
|
||||
}
|
||||
if (value === 0) {
|
||||
const message = 'Target Size In Base Asset cannot be zero. Fix this please.'
|
||||
badDefinitionUnhandledException(undefined, message, tradingSystemStageNode.initialTargets.targetSizeInBaseAsset.formula)
|
||||
}
|
||||
|
||||
switch (tradingSystemStageNode.type) {
|
||||
case 'Open Stage': {
|
||||
tradingEngine.current.position.positionBaseAsset.entryTargetSize.value = global.PRECISE(value, 10)
|
||||
tradingEngine.current.position.positionQuotedAsset.entryTargetSize.value =
|
||||
global.PRECISE(value * tradingEngine.current.position.entryTargetRate.value, 10)
|
||||
break
|
||||
}
|
||||
case 'Close Stage': {
|
||||
tradingEngine.current.position.positionBaseAsset.exitTargetSize.value = global.PRECISE(value, 10)
|
||||
tradingEngine.current.position.positionQuotedAsset.exitTargetSize.value =
|
||||
global.PRECISE(value * tradingEngine.current.position.exitTargetRate.value, 10)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/* Remember how the end user defined this stage. */
|
||||
tradingEngineStageNode.stageDefinedIn.value = 'Base Asset'
|
||||
} else {
|
||||
const message = 'You need to specify a Formula for this.'
|
||||
badDefinitionUnhandledException(undefined, message, tradingSystemStageNode.initialTargets.targetSizeInBaseAsset)
|
||||
}
|
||||
}
|
||||
|
||||
/* Position In Quoted Asset */
|
||||
if (tradingSystemStageNode.initialTargets.targetSizeInQuotedAsset !== undefined) {
|
||||
if (tradingSystemStageNode.initialTargets.targetSizeInQuotedAsset.formula !== undefined) {
|
||||
let value = tradingSystem.formulas.get(tradingSystemStageNode.initialTargets.targetSizeInQuotedAsset.formula.id)
|
||||
if (value === undefined) {
|
||||
const message = 'Target Size In Quoted Asset cannot be undefined. Fix this please.'
|
||||
badDefinitionUnhandledException(undefined, message, tradingSystemStageNode.initialTargets.targetSizeInQuotedAsset.formula)
|
||||
}
|
||||
if (value === 0) {
|
||||
const message = 'Target Size In Quoted Asset cannot be zero. Fix this please.'
|
||||
badDefinitionUnhandledException(undefined, message, tradingSystemStageNode.initialTargets.targetSizeInQuotedAsset.formula)
|
||||
}
|
||||
switch (tradingSystemStageNode.type) {
|
||||
case 'Open Stage': {
|
||||
tradingEngine.current.position.positionQuotedAsset.entryTargetSize.value = global.PRECISE(value, 10)
|
||||
tradingEngine.current.position.positionBaseAsset.entryTargetSize.value =
|
||||
global.PRECISE(value / tradingEngine.current.position.entryTargetRate.value, 10)
|
||||
break
|
||||
}
|
||||
case 'Close Stage': {
|
||||
tradingEngine.current.position.positionQuotedAsset.exitTargetSize.value = global.PRECISE(value, 10)
|
||||
tradingEngine.current.position.positionBaseAsset.exitTargetSize.value =
|
||||
global.PRECISE(value / tradingEngine.current.position.exitTargetRate.value, 10)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/* Remember how the end user defined this stage. */
|
||||
tradingEngineStageNode.stageDefinedIn.value = 'Quoted Asset'
|
||||
} else {
|
||||
const message = 'You need to specify a Formula for this.'
|
||||
badDefinitionUnhandledException(undefined, message, tradingSystemStageNode.initialTargets.targetSizeInQuotedAsset)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateEnds() {
|
||||
if (tradingEngine.current.position.status.value === 'Open') {
|
||||
tradingEngine.current.position.end.value = tradingEngine.current.position.end.value + sessionParameters.timeFrame.config.value
|
||||
tradingEngine.current.position.endRate.value = tradingEngine.current.episode.candle.close.value
|
||||
tradingEngine.current.position.positionBaseAsset.endBalance.value = tradingEngine.current.episode.episodeBaseAsset.balance.value
|
||||
tradingEngine.current.position.positionQuotedAsset.endBalance.value = tradingEngine.current.episode.episodeQuotedAsset.balance.value
|
||||
}
|
||||
}
|
||||
|
||||
function resetTradingEngineDataStructure() {
|
||||
if (tradingEngine.current.position.status.value === 'Closed') {
|
||||
tradingEngineModule.initializeNode(tradingEngine.current.position)
|
||||
}
|
||||
}
|
||||
|
||||
function updateCounters() {
|
||||
if (tradingEngine.current.position.status.value === 'Open') {
|
||||
tradingEngine.current.position.positionCounters.periods.value++
|
||||
}
|
||||
}
|
||||
|
||||
function cycleBasedStatistics() {
|
||||
|
||||
calculateAssetsStatistics()
|
||||
calculatePositionStatistics()
|
||||
|
||||
function calculateAssetsStatistics() {
|
||||
/* Profit Loss Calculation */
|
||||
tradingEngine.current.position.positionBaseAsset.profitLoss.value =
|
||||
tradingEngine.current.episode.episodeBaseAsset.balance.value -
|
||||
tradingEngine.current.position.positionBaseAsset.beginBalance
|
||||
|
||||
tradingEngine.current.position.positionQuotedAsset.profitLoss.value =
|
||||
tradingEngine.current.episode.episodeQuotedAsset.balance.value -
|
||||
tradingEngine.current.position.positionQuotedAsset.beginBalance
|
||||
|
||||
tradingEngine.current.position.positionBaseAsset.profitLoss.value = global.PRECISE(tradingEngine.current.position.positionBaseAsset.profitLoss.value, 10)
|
||||
tradingEngine.current.position.positionQuotedAsset.profitLoss.value = global.PRECISE(tradingEngine.current.position.positionQuotedAsset.profitLoss.value, 10)
|
||||
|
||||
/* ROI Calculation */
|
||||
tradingEngine.current.position.positionBaseAsset.ROI.value =
|
||||
tradingEngine.current.position.positionBaseAsset.profitLoss.value * 100 /
|
||||
tradingEngine.current.strategyOpenStage.stageBaseAsset.targetSize.value
|
||||
|
||||
tradingEngine.current.position.positionQuotedAsset.ROI.value =
|
||||
tradingEngine.current.position.positionQuotedAsset.profitLoss.value * 100 /
|
||||
tradingEngine.current.strategyOpenStage.stageQuotedAsset.targetSize.value
|
||||
|
||||
tradingEngine.current.position.positionBaseAsset.ROI.value = global.PRECISE(tradingEngine.current.position.positionBaseAsset.ROI.value, 10)
|
||||
tradingEngine.current.position.positionQuotedAsset.ROI.value = global.PRECISE(tradingEngine.current.position.positionQuotedAsset.ROI.value, 10)
|
||||
|
||||
/* Hit Fail Calculation */
|
||||
if (tradingEngine.current.position.positionBaseAsset.ROI.value > 0) {
|
||||
tradingEngine.current.position.positionBaseAsset.hitFail.value = 'Hit'
|
||||
}
|
||||
if (tradingEngine.current.position.positionBaseAsset.ROI.value < 0) {
|
||||
tradingEngine.current.position.positionBaseAsset.hitFail.value = 'Fail'
|
||||
}
|
||||
if (tradingEngine.current.position.positionBaseAsset.ROI.value === 0) {
|
||||
tradingEngine.current.position.positionBaseAsset.hitFail.value = 'Even'
|
||||
}
|
||||
if (tradingEngine.current.position.positionQuotedAsset.ROI.value > 0) {
|
||||
tradingEngine.current.position.positionQuotedAsset.hitFail.value = 'Hit'
|
||||
}
|
||||
if (tradingEngine.current.position.positionQuotedAsset.ROI.value < 0) {
|
||||
tradingEngine.current.position.positionQuotedAsset.hitFail.value = 'Fail'
|
||||
}
|
||||
if (tradingEngine.current.position.positionQuotedAsset.ROI.value === 0) {
|
||||
tradingEngine.current.position.positionQuotedAsset.hitFail.value = 'Even'
|
||||
}
|
||||
}
|
||||
|
||||
function calculatePositionStatistics() {
|
||||
/* Profit Loss Calculation */
|
||||
tradingEngine.current.position.positionStatistics.profitLoss.value =
|
||||
(
|
||||
tradingEngine.current.episode.episodeBaseAsset.profitLoss.value * tradingEngine.current.position.endRate.value +
|
||||
tradingEngine.current.episode.episodeQuotedAsset.profitLoss.value
|
||||
)
|
||||
|
||||
tradingEngine.current.position.positionStatistics.profitLoss.value = global.PRECISE(tradingEngine.current.position.positionStatistics.profitLoss.value, 10)
|
||||
|
||||
/* ROI Calculation */
|
||||
tradingEngine.current.position.positionStatistics.ROI.value =
|
||||
(
|
||||
tradingEngine.current.position.positionStatistics.profitLoss.value
|
||||
)
|
||||
* 100 /
|
||||
(
|
||||
tradingEngine.current.position.positionBaseAsset.beginBalance * tradingEngine.current.position.beginRate.value +
|
||||
tradingEngine.current.position.positionQuotedAsset.beginBalance
|
||||
)
|
||||
tradingEngine.current.position.positionStatistics.ROI.value = global.PRECISE(tradingEngine.current.position.positionStatistics.ROI.value, 10)
|
||||
|
||||
/* Hit Fail Calculation */
|
||||
if (tradingEngine.current.position.positionStatistics.ROI.value > 0) {
|
||||
tradingEngine.current.position.positionStatistics.hitFail.value = 'Hit'
|
||||
}
|
||||
if (tradingEngine.current.position.positionStatistics.ROI.value < 0) {
|
||||
tradingEngine.current.position.positionStatistics.hitFail.value = 'Fail'
|
||||
}
|
||||
if (tradingEngine.current.position.positionStatistics.ROI.value === 0) {
|
||||
tradingEngine.current.position.positionStatistics.hitFail.value = 'Even'
|
||||
}
|
||||
|
||||
/* Days Calculation */
|
||||
tradingEngine.current.position.positionStatistics.days.value =
|
||||
tradingEngine.current.position.positionCounters.periods.value *
|
||||
sessionParameters.timeFrame.config.value / global.ONE_DAY_IN_MILISECONDS
|
||||
|
||||
tradingEngine.current.position.positionStatistics.days.value = global.PRECISE(tradingEngine.current.position.positionStatistics.days.value, 10)
|
||||
}
|
||||
}
|
||||
|
||||
function badDefinitionUnhandledException(err, message, node) {
|
||||
tradingSystem.errors.push([node.id, message])
|
||||
|
||||
logger.write(MODULE_NAME, "[ERROR] -> " + message);
|
||||
logger.write(MODULE_NAME, "[ERROR] -> node.name = " + node.name);
|
||||
logger.write(MODULE_NAME, "[ERROR] -> node.type = " + node.type);
|
||||
logger.write(MODULE_NAME, "[ERROR] -> node.config = " + JSON.stringify(node.config));
|
||||
if (err !== undefined) {
|
||||
logger.write(MODULE_NAME, "[ERROR] -> err.stack = " + err.stack);
|
||||
}
|
||||
throw 'Please fix the problem and try again.'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,786 @@
|
||||
exports.newTradingProcess = function newTradingProcess(bot, logger, UTILITIES) {
|
||||
/*
|
||||
This Module will load all the process data dependencies from files and send them downstream.
|
||||
After execution, will save the time range and status report of the process.
|
||||
*/
|
||||
const FULL_LOG = true;
|
||||
const MODULE_NAME = "Trading Process";
|
||||
const GMT_SECONDS = ':00.000 GMT+0000';
|
||||
|
||||
thisObject = {
|
||||
initialize: initialize,
|
||||
finalize: finalize,
|
||||
start: start
|
||||
};
|
||||
|
||||
let utilities = UTILITIES.newCloudUtilities(logger);
|
||||
|
||||
let statusDependencies;
|
||||
let dataDependenciesModule;
|
||||
let dataFiles = new Map();
|
||||
let multiPeriodDataFiles = new Map();
|
||||
|
||||
const FILE_STORAGE = require('../FileStorage.js');
|
||||
let fileStorage = FILE_STORAGE.newFileStorage(logger);
|
||||
|
||||
const TRADING_ENGINE_MODULE = require('./TradingEngine.js')
|
||||
let tradingEngineModule = TRADING_ENGINE_MODULE.newTradingEngine(bot, logger)
|
||||
|
||||
let TRADING_OUTPUT_MODULE = require("./TradingOutput")
|
||||
let tradingOutputModule = TRADING_OUTPUT_MODULE.newTradingOutput(bot, logger, tradingEngineModule, UTILITIES, FILE_STORAGE)
|
||||
|
||||
let processConfig;
|
||||
|
||||
return thisObject;
|
||||
|
||||
function initialize(pProcessConfig, pStatusDependencies, pDataDependencies, callBackFunction) {
|
||||
try {
|
||||
logger.fileName = MODULE_NAME
|
||||
logger.initialize()
|
||||
|
||||
statusDependencies = pStatusDependencies
|
||||
dataDependenciesModule = pDataDependencies
|
||||
processConfig = pProcessConfig
|
||||
|
||||
tradingOutputModule.initialize()
|
||||
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE)
|
||||
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] initialize -> err = " + err.stack);
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
}
|
||||
}
|
||||
|
||||
function finalize() {
|
||||
|
||||
tradingEngineModule.finalize()
|
||||
tradingEngineModule = undefined
|
||||
|
||||
tradingOutputModule.finalize()
|
||||
tradingOutputModule = undefined
|
||||
|
||||
dataFiles = undefined
|
||||
multiPeriodDataFiles = undefined
|
||||
statusDependencies = undefined
|
||||
dataDependenciesModule = undefined
|
||||
fileStorage = undefined
|
||||
processConfig = undefined
|
||||
thisObject = undefined
|
||||
}
|
||||
|
||||
async function start(callBackFunction) {
|
||||
try {
|
||||
let currentTimeFrame
|
||||
let currentTimeFrameLabel
|
||||
|
||||
/* Context Variables */
|
||||
let contextVariables = {
|
||||
lastFile: undefined, // Datetime of the last file files sucessfully produced by this process.
|
||||
dateBeginOfMarket: undefined, // Datetime of the first trade file in the whole market history.
|
||||
dateEndOfMarket: undefined // Datetime of the last file available to be used as an input of this process.
|
||||
};
|
||||
|
||||
if (getContextVariables() !== true) { return }
|
||||
|
||||
if (bot.FIRST_EXECUTION === true && bot.RESUME === false) {
|
||||
/*
|
||||
Here is where the Trading Engine and Trading Systems received are moved to the simulation state.
|
||||
*/
|
||||
bot.simulationState.tradingEngine = bot.TRADING_ENGINE
|
||||
bot.simulationState.tradingSystem = bot.TRADING_SYSTEM
|
||||
}
|
||||
|
||||
/* We set up the Trading Engine Module. */
|
||||
tradingEngineModule.initialize()
|
||||
|
||||
/* Initializing the Trading Process Date */
|
||||
if (bot.FIRST_EXECUTION === true && bot.RESUME === false) {
|
||||
/*
|
||||
This funcion is going to be called many times by the Trading Bot Loop.
|
||||
Only during the first execution and when the User is not resuming the execution
|
||||
of a stopped session / task; we are going to initialize the Process Date Time.
|
||||
This variable tell us which day we are standing at, specially while working
|
||||
with Daily Files. From this Date is that we are going to load the Daily Files.
|
||||
*/
|
||||
bot.simulationState.tradingEngine.current.episode.processDate.value = global.REMOVE_TIME(bot.TRADING_SESSION.tradingParameters.timeRange.config.initialDatetime).valueOf()
|
||||
}
|
||||
|
||||
/*
|
||||
This is the Date that is going to be used across the execution of this Trading Process.
|
||||
We need this because it has a different life cycle than the processData stored at the
|
||||
Trading Engine data structure. This date has to remain the same during the whole execution
|
||||
of the Trading Process until the end, inclusind the writting of Data Ranges and Status Reports.
|
||||
The processDate of the Trading Engine data structure on the other hand can be changed during
|
||||
the simulation loop, once we discover that all candles from a certain date have benn processed.
|
||||
Here is the point where we sync one and the other.
|
||||
*/
|
||||
let tradingProcessDate = global.REMOVE_TIME(bot.simulationState.tradingEngine.current.episode.processDate.value)
|
||||
await processSingleFiles()
|
||||
|
||||
if (await processMarketFiles() === false) {
|
||||
bot.TRADING_SESSIONHeartBeat(undefined, undefined, 'Waiting for Data Mining to be run')
|
||||
callBackFunction(global.DEFAULT_RETRY_RESPONSE)
|
||||
return
|
||||
}
|
||||
/*
|
||||
This is the Data Structure used at the Simulation with all indicator data.
|
||||
We start creating it right here.
|
||||
*/
|
||||
let chart = {}
|
||||
/*
|
||||
Here we check if we need to get Daily Files or not. As an optimization, when
|
||||
we are running on a Time Frame of 1hs or above, we are not going to load
|
||||
dependencies on Daily Files. The way we recognize that is by checking if
|
||||
we alreaady set a value to currentTimeFrame. We are also not going to loop
|
||||
through days if we are processing market files.
|
||||
*/
|
||||
if (currentTimeFrame) {
|
||||
/* We are processing Market Files */
|
||||
/*
|
||||
With all the indicators data files loaded, we will build the chart object
|
||||
data structure that will be used in user-defied conditions and formulas.
|
||||
*/
|
||||
bot.TRADING_SESSIONHeartBeat(undefined, undefined, 'Waking up')
|
||||
buildCharts(chart)
|
||||
|
||||
if (checkThereAreCandles(chart) === true) {
|
||||
bot.TRADING_SESSIONHeartBeat(undefined, undefined, 'Running')
|
||||
await generateOutput(chart)
|
||||
bot.TRADING_SESSIONHeartBeat(undefined, undefined, 'Saving')
|
||||
await writeProcessFiles()
|
||||
bot.TRADING_SESSIONHeartBeat(undefined, undefined, 'Sleeping')
|
||||
} else {
|
||||
bot.TRADING_SESSIONHeartBeat(undefined, undefined, 'Waiting for Data Mining to be up to date. No candles found at.')
|
||||
callBackFunction(global.DEFAULT_RETRY_RESPONSE)
|
||||
return
|
||||
}
|
||||
|
||||
} else {
|
||||
/* We are processing Daily Files */
|
||||
do {
|
||||
global.EMIT_SESSION_STATUS(bot.TRADING_SESSION_STATUS, bot.TRADING_SESSIONKey)
|
||||
/*
|
||||
We update the Trading Process Date with the date calculated at the simulation.
|
||||
We will use this date to load indicator and output files. After that we will
|
||||
use it to save Output Files and later the Data Ranges. This is the point where
|
||||
the date calculated by the Simulation is applied at the Trading Process Level.
|
||||
*/
|
||||
tradingProcessDate = global.REMOVE_TIME(bot.simulationState.tradingEngine.current.episode.processDate.value)
|
||||
|
||||
if (checkStopTaskGracefully() === false) { break }
|
||||
if (checkStopProcessing() === false) { break }
|
||||
|
||||
bot.TRADING_SESSIONHeartBeat(undefined, undefined, 'Waking up')
|
||||
if (await processDailyFiles() === false) {
|
||||
bot.TRADING_SESSIONHeartBeat(undefined, undefined, 'Waiting for Data Mining to be run')
|
||||
callBackFunction(global.DEFAULT_RETRY_RESPONSE)
|
||||
return
|
||||
}
|
||||
/*
|
||||
With all the indicators data files loaded, we will build the chart object
|
||||
data structure that will be used in user-defied conditions and formulas.
|
||||
*/
|
||||
buildCharts(chart)
|
||||
/*
|
||||
The process of generating the output includes the trading simulation.
|
||||
*/
|
||||
if (checkThereAreCandles(chart) === true) {
|
||||
bot.TRADING_SESSIONHeartBeat(undefined, undefined, 'Running')
|
||||
await generateOutput(chart)
|
||||
bot.TRADING_SESSIONHeartBeat(undefined, undefined, 'Saving')
|
||||
await writeProcessFiles()
|
||||
bot.TRADING_SESSIONHeartBeat(undefined, undefined, 'Sleeping')
|
||||
} else {
|
||||
bot.TRADING_SESSIONHeartBeat(undefined, undefined, 'Waiting for Data Mining to be up to date')
|
||||
callBackFunction(global.DEFAULT_RETRY_RESPONSE)
|
||||
return
|
||||
}
|
||||
/*
|
||||
If for any reason the session was stopped, we will break this loop and exit the process.
|
||||
*/
|
||||
if (bot.STOP_SESSION === true) { break }
|
||||
/*
|
||||
When we get to the end of the market, we need to break this process loop in order
|
||||
to let time pass, new information be collected from the exchange, new data built
|
||||
into indicators, and eventually a new execution of this process.
|
||||
*/
|
||||
if (checkStopHeadOfTheMarket() === false) { break }
|
||||
|
||||
}
|
||||
while (true)
|
||||
}
|
||||
|
||||
checkIfSessionMustStop()
|
||||
/*
|
||||
Everything worked as expected. We return an OK code and wait for
|
||||
the Bot Loop to call us again later.
|
||||
*/
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE)
|
||||
|
||||
function checkThereAreCandles(chart) {
|
||||
let sessionParameters = bot.TRADING_SESSION.tradingParameters
|
||||
let propertyName = 'at' + sessionParameters.timeFrame.config.label.replace('-', '')
|
||||
let candles = chart[propertyName].candles
|
||||
|
||||
if (candles.length === 0) {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
function getContextVariables() {
|
||||
try {
|
||||
let thisReport;
|
||||
let statusReport;
|
||||
|
||||
/* We are going to use the start date as beging of market date. */
|
||||
contextVariables.dateBeginOfMarket = global.REMOVE_TIME(bot.TRADING_SESSION.tradingParameters.timeRange.config.initialDatetime)
|
||||
/*
|
||||
Here we get the status report from the bot who knows which is the end of the market.
|
||||
*/
|
||||
statusReport = statusDependencies.reportsByMainUtility.get("Market Ending Point")
|
||||
|
||||
if (statusReport === undefined) { // This means the status report does not exist, that could happen for instance at the begining of a month.
|
||||
logger.write(MODULE_NAME, "[WARN] start -> getContextVariables -> Status Report does not exist. Retrying Later. ");
|
||||
callBackFunction(global.DEFAULT_RETRY_RESPONSE);
|
||||
return false
|
||||
}
|
||||
|
||||
if (statusReport.status === "Status Report is corrupt.") {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> getContextVariables -> Can not continue because dependecy Status Report is corrupt. ");
|
||||
callBackFunction(global.DEFAULT_RETRY_RESPONSE);
|
||||
return false
|
||||
}
|
||||
|
||||
thisReport = statusReport.file;
|
||||
|
||||
if (thisReport.lastFile === undefined) {
|
||||
logger.write(MODULE_NAME, "[WARN] start -> getContextVariables -> Undefined Last File. -> thisReport = " + JSON.stringify(thisReport));
|
||||
|
||||
let customOK = {
|
||||
result: global.CUSTOM_OK_RESPONSE.result,
|
||||
message: "Dependency not ready."
|
||||
}
|
||||
logger.write(MODULE_NAME, "[WARN] start -> getContextVariables -> customOK = " + customOK.message);
|
||||
callBackFunction(customOK);
|
||||
return false
|
||||
}
|
||||
|
||||
contextVariables.dateEndOfMarket = new Date(thisReport.lastFile.valueOf());
|
||||
|
||||
/* Finally we get our own Status Report. */
|
||||
statusReport = statusDependencies.reportsByMainUtility.get("Self Reference")
|
||||
|
||||
if (statusReport === undefined) { // This means the status report does not exist, that could happen for instance at the begining of a month.
|
||||
logger.write(MODULE_NAME, "[WARN] start -> getContextVariables -> Status Report does not exist. Retrying Later. ");
|
||||
callBackFunction(global.DEFAULT_RETRY_RESPONSE);
|
||||
return false
|
||||
}
|
||||
|
||||
if (statusReport.status === "Status Report is corrupt.") {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> getContextVariables -> Can not continue because self dependecy Status Report is corrupt. Aborting Process.");
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
return false
|
||||
}
|
||||
|
||||
thisReport = statusReport.file;
|
||||
bot.simulationState = thisReport.simulationState;
|
||||
if (bot.simulationState === undefined) { bot.simulationState = {} } // This should happen only when there is no status report
|
||||
|
||||
if (thisReport.lastFile !== undefined) {
|
||||
if (bot.RESUME !== true) {
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> getContextVariables -> Starting from the begining because bot has just started and resume execution was true."); }
|
||||
startFromBegining();
|
||||
return true
|
||||
}
|
||||
contextVariables.lastFile = new Date(thisReport.lastFile);
|
||||
return true
|
||||
|
||||
} else {
|
||||
/*
|
||||
We are here becuse either:
|
||||
1. There is no status report
|
||||
2. There is no Last File (this happens on Market Files)
|
||||
*/
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> getContextVariables -> Starting from the begining of the market because own status report not found or lastFile was undefined."); }
|
||||
startFromBegining();
|
||||
return true
|
||||
}
|
||||
|
||||
function startFromBegining() {
|
||||
contextVariables.lastFile = new Date(contextVariables.dateBeginOfMarket.getUTCFullYear() + "-" + (contextVariables.dateBeginOfMarket.getUTCMonth() + 1) + "-" + contextVariables.dateBeginOfMarket.getUTCDate() + " " + "00:00" + GMT_SECONDS);
|
||||
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> getContextVariables -> startFromBegining -> contextVariables.lastFile = " + contextVariables.lastFile); }
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> getContextVariables -> err = " + err.stack);
|
||||
if (err.message === "Cannot read property 'file' of undefined") {
|
||||
logger.write(MODULE_NAME, "[HINT] start -> getContextVariables -> Check the bot configuration to see if all of its statusDependencies declarations are correct. ");
|
||||
logger.write(MODULE_NAME, "[HINT] start -> getContextVariables -> Dependencies loaded -> keys = " + JSON.stringify(statusDependencies.keys));
|
||||
}
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
}
|
||||
}
|
||||
|
||||
async function processSingleFiles() {
|
||||
dataFiles = new Map();
|
||||
|
||||
for (let dependencyIndex = 0; dependencyIndex < dataDependenciesModule.nodeArray.length; dependencyIndex++) {
|
||||
let dependency = dataDependenciesModule.nodeArray[dependencyIndex];
|
||||
let datasetModule = dataDependenciesModule.dataSetsModulesArray[dependencyIndex];
|
||||
|
||||
if (datasetModule.node.config.codeName !== "Single-File") {
|
||||
continue
|
||||
}
|
||||
|
||||
if (dataDependenciesModule.isItADepenency('atAnyTimeFrame', datasetModule.node.parentNode.config.singularVariableName) !== true) {
|
||||
continue
|
||||
}
|
||||
|
||||
let fileName = "Data.json";
|
||||
let filePath = datasetModule.node.parentNode.config.codeName + '/' + datasetModule.node.config.codeName;
|
||||
|
||||
/* We cut the async calls via callBacks at this point, so as to have a clearer code upstream */
|
||||
let response = await asyncGetDatasetFile(datasetModule, filePath, fileName)
|
||||
|
||||
if (response.err.result !== global.DEFAULT_OK_RESPONSE.result) {
|
||||
throw (response.err)
|
||||
}
|
||||
|
||||
let dataFile = JSON.parse(response.text);
|
||||
dataFiles.set(dependency.id, dataFile);
|
||||
}
|
||||
|
||||
let mapKey = "Single Files"
|
||||
multiPeriodDataFiles.set(mapKey, dataFiles)
|
||||
}
|
||||
|
||||
async function processMarketFiles() {
|
||||
/*
|
||||
We do market files first since if the simulation is run on daily files, there will
|
||||
be a loop to get each of those files and we do not need that loop to reload market files.
|
||||
|
||||
We will iterate through all posible timeFrames.
|
||||
*/
|
||||
for (let n = 0; n < global.marketFilesPeriods.length; n++) {
|
||||
const timeFrame = global.marketFilesPeriods[n][0];
|
||||
const timeFrameLabel = global.marketFilesPeriods[n][1];
|
||||
|
||||
dataFiles = new Map();
|
||||
|
||||
if (bot.TRADING_SESSION.tradingParameters.timeFrame.config.label === timeFrameLabel) {
|
||||
currentTimeFrame = global.marketFilesPeriods[n][0];
|
||||
currentTimeFrameLabel = global.marketFilesPeriods[n][1];
|
||||
}
|
||||
for (let dependencyIndex = 0; dependencyIndex < dataDependenciesModule.nodeArray.length; dependencyIndex++) {
|
||||
let dependency = dataDependenciesModule.nodeArray[dependencyIndex];
|
||||
let datasetModule = dataDependenciesModule.dataSetsModulesArray[dependencyIndex];
|
||||
|
||||
if (dependency.referenceParent.config.codeName !== "Multi-Period-Market") {
|
||||
continue
|
||||
}
|
||||
|
||||
if (dataDependenciesModule.isItADepenency(timeFrameLabel, datasetModule.node.parentNode.config.singularVariableName) !== true) {
|
||||
if (!(bot.TRADING_SESSION.tradingParameters.timeFrame.config.label === timeFrameLabel && datasetModule.node.parentNode.config.pluralVariableName === 'candles')) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
let fileName = "Data.json";
|
||||
let filePath = dependency.referenceParent.parentNode.config.codeName + '/' + dependency.referenceParent.config.codeName + "/" + timeFrameLabel;
|
||||
|
||||
/* We cut the async calls via callBacks at this point, so as to have a clearer code upstream */
|
||||
let response = await asyncGetDatasetFile(datasetModule, filePath, fileName)
|
||||
|
||||
if (response.err.message === 'File does not exist.') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (response.err.result !== global.DEFAULT_OK_RESPONSE.result) {
|
||||
throw (response.err)
|
||||
}
|
||||
|
||||
let dataFile = JSON.parse(response.text);
|
||||
let trimmedDataFile = trimDataFile(dataFile, datasetModule.node.parentNode.record)
|
||||
dataFiles.set(dependency.id, trimmedDataFile);
|
||||
|
||||
function trimDataFile(dataFile, recordDefinition) {
|
||||
/*
|
||||
Here we will discard all the records in a file that are outside of the current time range.
|
||||
We will include the las element previous to the begining of the time range. This is needed
|
||||
because during the simulation, the current period is not the open one, but the previous to
|
||||
the open, and if we do not include the previous to the initial datetime there will be no
|
||||
current objects at the begining of the simulation for many time frames.
|
||||
*/
|
||||
let beginIndex
|
||||
let endIndex
|
||||
let result = []
|
||||
for (let i = 0; i < recordDefinition.properties.length; i++) {
|
||||
let property = recordDefinition.properties[i]
|
||||
if (property.config.codeName === 'begin') {
|
||||
beginIndex = i
|
||||
}
|
||||
if (property.config.codeName === 'end') {
|
||||
endIndex = i
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < dataFile.length; i++) {
|
||||
let dataRecord = dataFile[i]
|
||||
let begin = dataRecord[beginIndex]
|
||||
let end = dataRecord[endIndex]
|
||||
if (end + timeFrame < bot.TRADING_SESSION.tradingParameters.timeRange.config.initialDatetime - 1) { continue } // /1 because we need the previous closed element
|
||||
if (begin > bot.TRADING_SESSION.tradingParameters.timeRange.config.finalDatetime) { continue }
|
||||
result.push(dataRecord)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
let mapKey = global.marketFilesPeriods[n][1];
|
||||
multiPeriodDataFiles.set(mapKey, dataFiles)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
async function processDailyFiles() {
|
||||
/* Telling the world we are alive and doing well and which date we are processing right now. */
|
||||
let processingDateString = tradingProcessDate.getUTCFullYear() + '-' + utilities.pad(tradingProcessDate.getUTCMonth() + 1, 2) + '-' + utilities.pad(tradingProcessDate.getUTCDate(), 2);
|
||||
bot.processHeartBeat(processingDateString, undefined, "Running...")
|
||||
|
||||
/*
|
||||
We will iterate through all posible timeFrames.
|
||||
*/
|
||||
for (let n = 0; n < global.dailyFilePeriods.length; n++) {
|
||||
const timeFrame = global.dailyFilePeriods[n][0];
|
||||
const timeFrameLabel = global.dailyFilePeriods[n][1];
|
||||
|
||||
if (processConfig.framework.validtimeFrames !== undefined) {
|
||||
let validPeriod = false;
|
||||
for (let i = 0; i < processConfig.framework.validtimeFrames.length; i++) {
|
||||
let period = processConfig.framework.validtimeFrames[i];
|
||||
if (period === timeFrameLabel) { validPeriod = true }
|
||||
}
|
||||
if (validPeriod === false) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if (bot.TRADING_SESSION.tradingParameters.timeFrame.config.label === timeFrameLabel) {
|
||||
currentTimeFrame = global.dailyFilePeriods[n][0];
|
||||
currentTimeFrameLabel = global.dailyFilePeriods[n][1];
|
||||
}
|
||||
|
||||
dataFiles = new Map();
|
||||
|
||||
/*
|
||||
We will iterate through all dependencies, in order to load the
|
||||
files that later will end up at the chart data structure.
|
||||
*/
|
||||
for (let dependencyIndex = 0; dependencyIndex < dataDependenciesModule.nodeArray.length; dependencyIndex++) {
|
||||
let dependency = dataDependenciesModule.nodeArray[dependencyIndex];
|
||||
let datasetModule = dataDependenciesModule.dataSetsModulesArray[dependencyIndex];
|
||||
|
||||
if (dependency.referenceParent.config.codeName !== "Multi-Period-Daily") {
|
||||
continue
|
||||
}
|
||||
|
||||
if (dataDependenciesModule.isItADepenency(timeFrameLabel, datasetModule.node.parentNode.config.singularVariableName) !== true) {
|
||||
if (!(bot.TRADING_SESSION.tradingParameters.timeFrame.config.label === timeFrameLabel && datasetModule.node.parentNode.config.pluralVariableName === 'candles')) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
let dateForPath = tradingProcessDate.getUTCFullYear() + '/' + utilities.pad(tradingProcessDate.getUTCMonth() + 1, 2) + '/' + utilities.pad(tradingProcessDate.getUTCDate(), 2);
|
||||
let filePath
|
||||
if (dependency.referenceParent.config.codeName === "Multi-Period-Daily") {
|
||||
filePath = dependency.referenceParent.parentNode.config.codeName + '/' + dependency.referenceParent.config.codeName + "/" + timeFrameLabel + "/" + dateForPath;
|
||||
} else {
|
||||
filePath = dependency.referenceParent.parentNode.config.codeName + '/' + dependency.referenceParent.config.codeName + "/" + dateForPath;
|
||||
}
|
||||
let fileName = "Data.json";
|
||||
|
||||
/* We cut the async calls via callBacks at this point, so as to have a clearer code upstream */
|
||||
let response = await asyncGetDatasetFile(datasetModule, filePath, fileName)
|
||||
|
||||
if (response.err.message === 'File does not exist.') {
|
||||
return false
|
||||
}
|
||||
if (response.err.result !== global.DEFAULT_OK_RESPONSE.result) {
|
||||
throw (response.err)
|
||||
}
|
||||
|
||||
let dataFile = JSON.parse(response.text);
|
||||
dataFiles.set(dependency.id, dataFile);
|
||||
}
|
||||
|
||||
let mapKey = global.dailyFilePeriods[n][1];
|
||||
multiPeriodDataFiles.set(mapKey, dataFiles)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function checkStopHeadOfTheMarket() {
|
||||
/*
|
||||
We need to check if we have reached the head of the market in order to know
|
||||
when to break the Daily Files Process loop and give time for a new candles /
|
||||
indicators to be built and continue the processing once this process is called
|
||||
again.
|
||||
*/
|
||||
if (bot.simulationState.tradingEngine.current.episode.headOfTheMarket.value === true) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function checkStopTaskGracefully() {
|
||||
/* Validation that we dont need to stop. */
|
||||
if (global.STOP_TASK_GRACEFULLY === true) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function checkStopProcessing() {
|
||||
/* Validation that we dont need to stop. */
|
||||
if (bot.STOP_SESSION === true) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function buildCharts(chart) {
|
||||
const COMMONS = require('../Commons.js')
|
||||
let commons = COMMONS.newCommons(bot, logger, UTILITIES, FILE_STORAGE)
|
||||
|
||||
let mainDependency = {}
|
||||
|
||||
/* The first phase here is about checking that we have everything we need at the definition level. */
|
||||
let dataDependencies = global.NODE_BRANCH_TO_ARRAY(bot.processNode.referenceParent.processDependencies, 'Data Dependency')
|
||||
/*
|
||||
We will filter ourt declared dependencies that are not present in the workspace.
|
||||
This will allow the user to have less Data Mines loaded at the workspace
|
||||
that the ones that a Trading Mine depends on.
|
||||
*/
|
||||
dataDependencies = global.FILTER_OUT_NODES_WITHOUT_REFERENCE_PARENT_FROM_NODE_ARRAY(dataDependencies)
|
||||
|
||||
if (commons.validateDataDependencies(dataDependencies, callBackFunction) !== true) { return }
|
||||
|
||||
let outputDatasets = global.NODE_BRANCH_TO_ARRAY(bot.processNode.referenceParent.processOutput, 'Output Dataset')
|
||||
if (commons.validateOutputDatasets(outputDatasets, callBackFunction) !== true) { return }
|
||||
|
||||
/* The second phase is about transforming the inputs into a format that can be used to apply the user defined code. */
|
||||
for (let j = 0; j < global.marketFilesPeriods.length; j++) {
|
||||
let timeFrameLabel = marketFilesPeriods[j][1]
|
||||
let dataFiles = multiPeriodDataFiles.get(timeFrameLabel)
|
||||
let products = {}
|
||||
|
||||
if (dataFiles !== undefined) {
|
||||
commons.inflateDatafiles(dataFiles, dataDependencies, products, mainDependency, currentTimeFrame)
|
||||
|
||||
let propertyName = 'at' + timeFrameLabel.replace('-', '')
|
||||
chart[propertyName] = products
|
||||
}
|
||||
}
|
||||
|
||||
for (let j = 0; j < global.dailyFilePeriods.length; j++) {
|
||||
let timeFrameLabel = global.dailyFilePeriods[j][1]
|
||||
let dataFiles = multiPeriodDataFiles.get(timeFrameLabel)
|
||||
let products = {}
|
||||
|
||||
if (dataFiles !== undefined) {
|
||||
commons.inflateDatafiles(dataFiles, dataDependencies, products, mainDependency, currentTimeFrame)
|
||||
|
||||
let propertyName = 'at' + timeFrameLabel.replace('-', '')
|
||||
chart[propertyName] = products
|
||||
}
|
||||
}
|
||||
|
||||
/* Single Files */
|
||||
let dataFiles = multiPeriodDataFiles.get('Single Files')
|
||||
let products = {}
|
||||
|
||||
if (dataFiles !== undefined) {
|
||||
commons.inflateDatafiles(dataFiles, dataDependencies, products, mainDependency, currentTimeFrame)
|
||||
|
||||
let propertyName = 'atAnyTimeFrame'
|
||||
chart[propertyName] = products
|
||||
}
|
||||
}
|
||||
|
||||
async function generateOutput(chart) {
|
||||
await tradingOutputModule.start(
|
||||
chart,
|
||||
currentTimeFrame,
|
||||
currentTimeFrameLabel,
|
||||
tradingProcessDate
|
||||
)
|
||||
|
||||
/*
|
||||
From here on, all other loops executions wont be the first execution and also
|
||||
we will consider that it is not resuming a previous execution as well.
|
||||
*/
|
||||
bot.FIRST_EXECUTION = false
|
||||
bot.RESUME = false
|
||||
}
|
||||
|
||||
async function writeProcessFiles() {
|
||||
let outputDatasets = global.NODE_BRANCH_TO_ARRAY(bot.processNode.referenceParent.processOutput, 'Output Dataset')
|
||||
|
||||
await writeTimeFramesFiles(currentTimeFrame, currentTimeFrameLabel)
|
||||
await writeDataRanges()
|
||||
|
||||
if (currentTimeFrame > global.dailyFilePeriods[0][0]) {
|
||||
await writeMarketStatusReport(currentTimeFrameLabel)
|
||||
} else {
|
||||
await writeDailyStatusReport(currentTimeFrameLabel)
|
||||
}
|
||||
|
||||
async function writeDataRanges() {
|
||||
for (
|
||||
let outputDatasetIndex = 0;
|
||||
outputDatasetIndex < outputDatasets.length;
|
||||
outputDatasetIndex++
|
||||
) {
|
||||
let productCodeName = outputDatasets[outputDatasetIndex].referenceParent.parentNode.config.codeName;
|
||||
await writeDataRange(productCodeName);
|
||||
}
|
||||
|
||||
async function writeDataRange(productCodeName) {
|
||||
let dataRange = {
|
||||
begin: contextVariables.dateBeginOfMarket.valueOf(),
|
||||
end: tradingProcessDate.valueOf() + global.ONE_DAY_IN_MILISECONDS
|
||||
}
|
||||
|
||||
let fileContent = JSON.stringify(dataRange);
|
||||
let fileName = '/Data.Range.json';
|
||||
let filePath = bot.filePathRoot + "/Output/" + bot.TRADING_SESSION.folderName + "/" + productCodeName + "/" + 'Multi-Period-Daily' + fileName;
|
||||
|
||||
let response = await fileStorage.asyncCreateTextFile(filePath, fileContent + '\n')
|
||||
|
||||
if (response.err.result !== global.DEFAULT_OK_RESPONSE.result) {
|
||||
throw (response.err)
|
||||
}
|
||||
|
||||
/*
|
||||
Raise the event that the Data Range was Updated.
|
||||
*/
|
||||
let key = bot.dataMine + "-" + bot.codeName + "-" + productCodeName + "-" + bot.exchange + "-" + bot.market.baseAsset + '/' + bot.market.quotedAsset
|
||||
let event = {
|
||||
dateRange: dataRange
|
||||
}
|
||||
|
||||
global.EVENT_SERVER_CLIENT.raiseEvent(key, 'Data Range Updated', event)
|
||||
}
|
||||
}
|
||||
|
||||
async function writeTimeFramesFiles(currentTimeFrame, currentTimeFrameLabel) {
|
||||
|
||||
for (
|
||||
let outputDatasetIndex = 0;
|
||||
outputDatasetIndex < outputDatasets.length;
|
||||
outputDatasetIndex++
|
||||
) {
|
||||
let productCodeName = outputDatasets[outputDatasetIndex].referenceParent.parentNode.config.codeName;
|
||||
await writeTimeFramesFile(currentTimeFrameLabel, productCodeName, 'Multi-Period-Daily')
|
||||
await writeTimeFramesFile(currentTimeFrameLabel, productCodeName, 'Multi-Period-Market')
|
||||
|
||||
async function writeTimeFramesFile(currentTimeFrameLabel, productCodeName, processType) {
|
||||
|
||||
let timeFramesArray = []
|
||||
timeFramesArray.push(currentTimeFrameLabel)
|
||||
|
||||
let fileContent = JSON.stringify(timeFramesArray)
|
||||
let fileName = '/Time.Frames.json';
|
||||
|
||||
let filePath = bot.filePathRoot + "/Output/" + bot.TRADING_SESSION.folderName + "/" + productCodeName + "/" + processType + fileName;
|
||||
|
||||
let response = await fileStorage.asyncCreateTextFile(filePath, fileContent + '\n')
|
||||
if (response.err.result !== global.DEFAULT_OK_RESPONSE.result) {
|
||||
throw (response.err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function writeDailyStatusReport(currentTimeFrameLabel) {
|
||||
let reportKey = bot.dataMine + "-" + bot.codeName + "-" + bot.processNode.referenceParent.config.codeName
|
||||
let thisReport = statusDependencies.statusReports.get(reportKey)
|
||||
|
||||
thisReport.file.lastExecution = bot.currentDaytime
|
||||
thisReport.file.lastFile = tradingProcessDate
|
||||
thisReport.file.simulationState = bot.simulationState
|
||||
thisReport.file.timeFrames = currentTimeFrameLabel
|
||||
await thisReport.asyncSave()
|
||||
}
|
||||
|
||||
async function writeMarketStatusReport(currentTimeFrameLabel) {
|
||||
let reportKey = bot.dataMine + "-" + bot.codeName + "-" + bot.processNode.referenceParent.config.codeName
|
||||
let thisReport = statusDependencies.statusReports.get(reportKey);
|
||||
|
||||
thisReport.file.lastExecution = bot.processDatetime
|
||||
thisReport.file.simulationState = bot.simulationState
|
||||
thisReport.file.timeFrames = currentTimeFrameLabel
|
||||
|
||||
logger.newInternalLoop(bot.codeName, bot.process, tradingProcessDate)
|
||||
await thisReport.asyncSave()
|
||||
}
|
||||
}
|
||||
|
||||
function checkIfSessionMustStop() {
|
||||
|
||||
if (bot.TRADING_SESSION.type === 'Backtesting Session') {
|
||||
/*
|
||||
Backtests needs only one execution of this process to complete.
|
||||
*/
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[IMPORTANT] checkIfSessionMustStop -> Backtesting Session Finished. Stopping the Session now. ') }
|
||||
bot.TRADING_SESSION.stop('Backtesting Session Finished.')
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
/* An unhandled exception occured. in this case we return Fail and log the stack. */
|
||||
if (err.stack) {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> Unhandled Exception. Will Abort this process. err = " + err.stack);
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
return
|
||||
}
|
||||
|
||||
/* Some expected file was not found. We will return a RETRY code and move on. */
|
||||
if ((err.result === "Fail Because" && err.message === "File does not exist.") || err.code === 'The specified key does not exist.') {
|
||||
logger.write(MODULE_NAME, "[ERROR] File not Found. Will Retry the Process Loop.")
|
||||
callBackFunction(global.DEFAULT_RETRY_RESPONSE);
|
||||
return
|
||||
}
|
||||
|
||||
/* Some other handled exception occured. We return Fail and move on. */
|
||||
if (err.result !== global.DEFAULT_OK_RESPONSE.result) {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> Handled Exception. Will Abort this process. err = " + err.message);
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function pad(str, max) {
|
||||
str = str.toString();
|
||||
return str.length < max ? pad(" " + str, max) : str;
|
||||
}
|
||||
|
||||
async function asyncGetDatasetFile(datasetModule, filePath, fileName) {
|
||||
|
||||
let promise = new Promise((resolve, reject) => {
|
||||
|
||||
datasetModule.getTextFile(filePath, fileName, onFileReceived);
|
||||
function onFileReceived(err, text) {
|
||||
|
||||
let response = {
|
||||
err: err,
|
||||
text: text
|
||||
}
|
||||
resolve(response)
|
||||
}
|
||||
});
|
||||
|
||||
return promise
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
exports.newTradingRecords = function newTradingRecords(bot, logger) {
|
||||
/*
|
||||
This module facilitates the appending of records to the output of the process.
|
||||
*/
|
||||
const MODULE_NAME = 'Trading Records'
|
||||
|
||||
let thisObject = {
|
||||
appendRecords: appendRecords,
|
||||
initialize: initialize,
|
||||
finalize: finalize
|
||||
}
|
||||
|
||||
let tradingEngine
|
||||
let tradingSystem
|
||||
let sessionParameters
|
||||
let outputDatasetsMap
|
||||
|
||||
return thisObject
|
||||
|
||||
function initialize(pOutputDatasetsMap) {
|
||||
tradingEngine = bot.simulationState.tradingEngine
|
||||
tradingSystem = bot.simulationState.tradingSystem
|
||||
sessionParameters = bot.TRADING_SESSION.tradingParameters
|
||||
outputDatasetsMap = pOutputDatasetsMap // These are the files turned into arrays, stored in a Map by Product codeName.
|
||||
}
|
||||
|
||||
function finalize() {
|
||||
tradingEngine = undefined
|
||||
tradingSystem = undefined
|
||||
sessionParameters = undefined
|
||||
outputDatasetsMap = undefined
|
||||
}
|
||||
|
||||
function appendRecords() {
|
||||
/*
|
||||
Here we add records to the output files. At the product config property nodePath
|
||||
we have a pointer to the node that have the information we need to extract.
|
||||
Later, based on the product record definition we will extract each individual value.
|
||||
*/
|
||||
let outputDatasets = global.NODE_BRANCH_TO_ARRAY(bot.processNode.referenceParent.processOutput, 'Output Dataset')
|
||||
|
||||
for (let i = 0; i < outputDatasets.length; i++) {
|
||||
let outputDatasetNode = outputDatasets[i]
|
||||
let dataset = outputDatasetNode.referenceParent
|
||||
let product = dataset.parentNode
|
||||
let outputDatasetArray = outputDatasetsMap.get(product.config.codeName)
|
||||
|
||||
if (bot.processingDailyFiles === true && dataset.config.type === 'Daily Files') {
|
||||
persistRecords()
|
||||
}
|
||||
|
||||
if (bot.processingDailyFiles === false && dataset.config.type === 'Market Files') {
|
||||
persistRecords()
|
||||
}
|
||||
|
||||
function persistRecords() {
|
||||
/*
|
||||
There are a few products configured to be saved only at an specific cycle.
|
||||
*/
|
||||
if (product.config.saveAtCycle !== undefined) {
|
||||
if (tradingEngine.current.episode.cycle.value !== product.config.saveAtCycle) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/* Clean the file from information of previous executions */
|
||||
//pruneOutputFile(product, outputDatasetArray, tradingEngine.current.episode.candle.end.value)
|
||||
|
||||
/* Clean Open Records */
|
||||
if (product.config.saveAsObjects === true) {
|
||||
for (let j = 0; j < product.record.properties.length; j++) {
|
||||
let recordProperty = product.record.properties[j]
|
||||
if (recordProperty.config.codeName === product.config.propertyNameThatDefinesObject) {
|
||||
/* Remove Open Records */
|
||||
spliceOpenRecords(j, product, outputDatasetArray)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
The product root can be a node or a node property of type array.
|
||||
*/
|
||||
let productRoot
|
||||
try {
|
||||
productRoot = eval(product.config.nodePath)
|
||||
} catch (err) {
|
||||
badDefinitionUnhandledException(err, 'Cannot get productRoot -> nodePath = ' + product.config.nodePath, product, undefined)
|
||||
}
|
||||
|
||||
if (product.config.nodePathType === 'array') {
|
||||
/*
|
||||
This means that the configured nodePath is not pointing to a node, but to a node property that is an array.
|
||||
For that reason we will assume that each element of the array is a record to be outputed
|
||||
*/
|
||||
for (let index = 0; index < productRoot.length; index++) {
|
||||
/*
|
||||
The Product Root Node is the root of the node hiriarchy from where we are going to extract the record values.
|
||||
*/
|
||||
let productRootNode = productRoot[index]
|
||||
let record = scanRecordDefinition(product, productRootNode, index)
|
||||
if (record !== undefined) {
|
||||
/*
|
||||
We will add the index value to the record itself, so that the plotter can know to which
|
||||
brach of the trading engine data structure it belongs.
|
||||
*/
|
||||
record.push(index)
|
||||
persistIndividualRecord(record, product, outputDatasetArray)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/*
|
||||
This means that the configured nodePath points to a single node, which is the one whose children constitutes
|
||||
the record to be saved at the output file.
|
||||
*/
|
||||
/*
|
||||
The Product Root Node is the root of the node hiriarchy from where we are going to extract the record values.
|
||||
*/
|
||||
let productRootNode = productRoot
|
||||
let record = scanRecordDefinition(product, productRootNode)
|
||||
if (record !== undefined) {
|
||||
persistIndividualRecord(record, product, outputDatasetArray)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function scanRecordDefinition(product, productRootNode, index) {
|
||||
|
||||
let record = []
|
||||
for (let j = 0; j < product.record.properties.length; j++) {
|
||||
let recordProperty = product.record.properties[j]
|
||||
/*
|
||||
The Property Root Node is the Root of the Hiriarchy branch we must find in order
|
||||
to get the node where we are going to extract the value. Initially
|
||||
we point it to the Product Root Node, because that is the default in case
|
||||
a property does not have at its configuration a different nodePath configured
|
||||
pointing to an specific Root for the property.
|
||||
*/
|
||||
let propertyRootNode = productRootNode
|
||||
/*
|
||||
If we find at the configuration a nodePath, then we take this path to find
|
||||
the Root Node specifically for this property only.
|
||||
*/
|
||||
if (recordProperty.config.nodePath !== undefined) {
|
||||
if (recordProperty.config.nodePath === "tradingEngine.exchangeOrders.marketBuyOrders.marketOrders[index].orderBaseAsset") {
|
||||
let a = tradingEngine
|
||||
}
|
||||
try {
|
||||
propertyRootNode = eval(recordProperty.config.nodePath)
|
||||
} catch (err) {
|
||||
badDefinitionUnhandledException(err, 'Error Evaluating Record Property nodePath -> nodePath = ' + recordProperty.config.nodePath, product, recordProperty)
|
||||
}
|
||||
}
|
||||
/*
|
||||
The Target Node is the node from where we are going to exctract the value.
|
||||
We will use the codeName of the Record Property to match it with
|
||||
any of the properties of the Root Node to get the Target Node.
|
||||
*/
|
||||
let targetNode
|
||||
try {
|
||||
targetNode = propertyRootNode[recordProperty.config.codeName]
|
||||
} catch (err) {
|
||||
badDefinitionUnhandledException(err, 'Error setting Target Node.', product, recordProperty)
|
||||
}
|
||||
/*
|
||||
If the codeName of the Record Property can not match the name of the property at
|
||||
the target node, the user can explicitly specify the property name at the configuration,
|
||||
and in those cases we need to use that. This happens when there are many Record Properties
|
||||
pointing to the same property at the Target Node.
|
||||
*/
|
||||
if (recordProperty.config.childProperty !== undefined) {
|
||||
targetNode = propertyRootNode[recordProperty.config.childProperty]
|
||||
}
|
||||
/*
|
||||
It can happen that intead of having a Node in targetNode what we have is an
|
||||
array of nodes. We need to pick one of the elements of the array and for that
|
||||
we use the Index value we find at the configuration of the Record Property.
|
||||
*/
|
||||
if (recordProperty.config.index !== undefined) {
|
||||
try {
|
||||
targetNode = targetNode[recordProperty.config.index]
|
||||
} catch (err) {
|
||||
badDefinitionUnhandledException(err, 'Error setting Target Node.', product, recordProperty)
|
||||
}
|
||||
}
|
||||
/*
|
||||
By Default the value is extracted from the value property of the Target Node.
|
||||
But it might happen the the Target Node does not exist for example when there is an Array
|
||||
of Nodes defined in the Record Properties but not all of them exist at the Root Node.
|
||||
We filter out those cases by not extracting the value from the value property.
|
||||
*/
|
||||
let value = 0 // This is a default value, since we do not want null in files because it breakes JSON format.
|
||||
if (targetNode !== undefined) {
|
||||
if (targetNode.type !== undefined) {
|
||||
/*
|
||||
In this case the Target Node is really a node (since it has a type), so we extract the value
|
||||
from its value property.
|
||||
*/
|
||||
value = targetNode.value
|
||||
|
||||
if (recordProperty.config.decimals !== undefined) {
|
||||
try {
|
||||
value = Number(value.toFixed(recordProperty.config.decimals))
|
||||
} catch (err) {
|
||||
badDefinitionUnhandledException(err, 'Error applying configured decimals.', product, recordProperty)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/*
|
||||
In this case the Target Node is not really node, but the value itself. Se we return this as the
|
||||
value of the Record Property.
|
||||
*/
|
||||
value = targetNode
|
||||
}
|
||||
}
|
||||
|
||||
/* We are not going to add records where the begin or end are missing */
|
||||
if (
|
||||
recordProperty.config.codeName === 'begin' && value === 0 ||
|
||||
recordProperty.config.codeName === 'end' && value === 0
|
||||
) { return }
|
||||
|
||||
if (recordProperty.config.isString !== true && Array.isArray(value) !== true) {
|
||||
value = safeNumericValue(value)
|
||||
}
|
||||
if (recordProperty.config.isString === true && Array.isArray(value) !== true) {
|
||||
value = safeStringValue(value)
|
||||
}
|
||||
record.push(value)
|
||||
}
|
||||
return record
|
||||
}
|
||||
|
||||
function persistIndividualRecord(record, product, outputDatasetArray) {
|
||||
|
||||
if (product.config.saveAsObjects === true) {
|
||||
/* For saving objects we need to take care of a different set of rules. */
|
||||
for (let j = 0; j < product.record.properties.length; j++) {
|
||||
let recordProperty = product.record.properties[j]
|
||||
if (recordProperty.config.codeName === product.config.propertyNameThatDefinesObject) {
|
||||
let propertyValue = record[j]
|
||||
|
||||
if (bot.processingDailyFiles) {
|
||||
if (product.config.doNotCutObjectInDays !== true) {
|
||||
/*
|
||||
By default we will cut objects in days.
|
||||
*/
|
||||
if (propertyValue !== product.config.propertyValueThatPreventsSavingObject) {
|
||||
outputDatasetArray.push(record)
|
||||
}
|
||||
} else {
|
||||
/*
|
||||
When dealing with Daily Files, we need to avoid to write an open object at the last 'candle' of the day,
|
||||
since the object will be duplicated on the next day. How do we know we are positioned at the last candle
|
||||
of the day? Easy: the end of the candle must be 1 millisecod before the next day. That happens at any
|
||||
time frame.
|
||||
*/
|
||||
let currentDay = new Date(tradingEngine.current.episode.candle.end.value)
|
||||
let nextDay = new Date(tradingEngine.current.episode.candle.end.value + 1)
|
||||
if (currentDay.getUTCDate() !== nextDay.getUTCDate()) {
|
||||
/*
|
||||
We will save the object only if it is closed, becasuse we are at the last candle of the day.
|
||||
*/
|
||||
if (propertyValue === product.config.propertyValueThatClosesObject) {
|
||||
outputDatasetArray.push(record)
|
||||
}
|
||||
} else {
|
||||
/*
|
||||
When we are not at the end of the day, we will save the object normally, like in market files.
|
||||
*/
|
||||
if (propertyValue !== product.config.propertyValueThatPreventsSavingObject) {
|
||||
outputDatasetArray.push(record)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
/*
|
||||
For Market Files we will add a record everytime that proeprty value does not match this
|
||||
*/
|
||||
if (propertyValue !== product.config.propertyValueThatPreventsSavingObject) {
|
||||
outputDatasetArray.push(record)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/*
|
||||
When we are not dealing with objects, we add every record to the existing file except
|
||||
for the ones that are filtered out at the Product Definition.
|
||||
*/
|
||||
if (product.config.codeName === "Trading-System-Errors") {
|
||||
let a = 1
|
||||
}
|
||||
if (product.config.propertyNameThatDefinesStatus !== undefined && product.config.propertyValueThatPreventsSavingObject !== undefined) {
|
||||
for (let j = 0; j < product.record.properties.length; j++) {
|
||||
let recordProperty = product.record.properties[j]
|
||||
if (recordProperty.config.codeName === product.config.propertyNameThatDefinesStatus) {
|
||||
let propertyValue = record[j]
|
||||
|
||||
if (product.config.propertyValueThatPreventsSavingObject === "->Empty Array->" && propertyValue.length === 0) {
|
||||
break
|
||||
}
|
||||
if (propertyValue !== product.config.propertyValueThatPreventsSavingObject) {
|
||||
outputDatasetArray.push(record)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
outputDatasetArray.push(record)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function safeNumericValue(value) {
|
||||
/*
|
||||
The purpose of this function is to check that value variable does not have a value that
|
||||
will later break the JSON format of files where this is going to be stored at.
|
||||
*/
|
||||
if (value === Infinity) {
|
||||
value = Number.MAX_SAFE_INTEGER
|
||||
}
|
||||
if (value === undefined) {
|
||||
value = 0
|
||||
}
|
||||
if (value === null) {
|
||||
value = 0
|
||||
}
|
||||
if (isNaN(value)) {
|
||||
value = 0
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
function safeStringValue(value) {
|
||||
/*
|
||||
The purpose of this function is to check that value variable does not have a value that
|
||||
will later break the JSON format of files where this is going to be stored at.
|
||||
*/
|
||||
if (value === undefined) {
|
||||
value = ''
|
||||
}
|
||||
if (value === null) {
|
||||
value = ''
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
function pruneOutputFile(product, outputFile, currentEnd) {
|
||||
if (outputFile.isPrunned === true) { return }
|
||||
/*
|
||||
When a session is resumed, we will be potentially reading output files belonging to a previous session execution.
|
||||
For that reason we need to prune all the records that are beyond the current candle. We do not delete everything
|
||||
because we might be resuming a stopped session, which is fine.
|
||||
*/
|
||||
for (let i = 0; i < outputFile.length; i++) {
|
||||
let record = outputFile[i]
|
||||
|
||||
for (let j = 0; j < product.record.properties.length; j++) {
|
||||
let recordProperty = product.record.properties[j]
|
||||
if (recordProperty.config.codeName === 'end') {
|
||||
let end = record[j]
|
||||
if (end >= currentEnd) {
|
||||
outputFile.splice(i, 1)
|
||||
/*
|
||||
This will execute the next prune call in the next iteration of the NodeJs event loop
|
||||
allowing for other callbacks to be executed. It also prevents the error
|
||||
'Maximum call stack size exceeded', since the call is not placed at the call stack.
|
||||
*/
|
||||
setImmediate(pruneOutputFile, product, outputFile, currentEnd)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
outputFile.isPrunned = true
|
||||
}
|
||||
|
||||
function spliceOpenRecords(j, product, outputDatasetArray) {
|
||||
/*
|
||||
Before adding records to the output file, we need to remove all open records,
|
||||
because we will be adding the same records later with potentially
|
||||
new or updated information.
|
||||
*/
|
||||
|
||||
for (let i = 0; i < outputDatasetArray.length; i++) {
|
||||
let dataRecord = outputDatasetArray[i]
|
||||
if (dataRecord !== undefined) {
|
||||
let dataRecordValue = dataRecord[j]
|
||||
if (dataRecordValue !== product.config.propertyValueThatClosesObject) {
|
||||
outputDatasetArray.splice(i, 1)
|
||||
spliceOpenRecords(j, product, outputDatasetArray)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function badDefinitionUnhandledException(err, message, product, recordProperty) {
|
||||
logger.write(MODULE_NAME, "[ERROR] appendRecords -> " + message);
|
||||
logger.write(MODULE_NAME, "[ERROR] appendRecords -> product.name = " + product.name);
|
||||
logger.write(MODULE_NAME, "[ERROR] appendRecords -> product.config = " + JSON.stringify(product.config));
|
||||
|
||||
if (recordProperty) {
|
||||
logger.write(MODULE_NAME, "[ERROR] appendRecords -> recordProperty.name = " + recordProperty.name);
|
||||
logger.write(MODULE_NAME, "[ERROR] appendRecords -> recordProperty.config.codeName = " + recordProperty.config.codeName);
|
||||
logger.write(MODULE_NAME, "[ERROR] appendRecords -> recordProperty.config = " + JSON.stringify(recordProperty.config));
|
||||
}
|
||||
|
||||
if (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] appendRecords -> err.stack = " + err.stack);
|
||||
}
|
||||
throw 'Can not continue with a Definition Error like this.'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,538 @@
|
||||
exports.newTradingSimulation = function newTradingSimulation(bot, logger, tradingEngineModule, UTILITIES) {
|
||||
/*
|
||||
This Module represents the trading simulacion. Escentially a loop through a set of candles and
|
||||
the execution at each loop cycle of the Trading System Protocol.
|
||||
*/
|
||||
const FULL_LOG = true
|
||||
const MODULE_NAME = 'Trading Simulation -> ' + bot.TRADING_SESSION.name
|
||||
|
||||
let thisObject = {
|
||||
finalize: finalize,
|
||||
runSimulation: runSimulation
|
||||
}
|
||||
|
||||
let utilities = UTILITIES.newCloudUtilities(bot, logger)
|
||||
|
||||
return thisObject
|
||||
|
||||
function finalize() {
|
||||
thisObject = undefined
|
||||
}
|
||||
|
||||
async function runSimulation(
|
||||
chart,
|
||||
outputDatasetsMap,
|
||||
writeFiles,
|
||||
) {
|
||||
try {
|
||||
|
||||
let tradingSystem = bot.simulationState.tradingSystem
|
||||
let tradingEngine = bot.simulationState.tradingEngine
|
||||
let sessionParameters = bot.TRADING_SESSION.tradingParameters
|
||||
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> initialDatetime = ' + sessionParameters.timeRange.config.initialDatetime) }
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> finalDatetime = ' + sessionParameters.timeRange.config.finalDatetime) }
|
||||
|
||||
/* These are the Modules we will need to run the Simulation */
|
||||
const TRADING_RECORDS_MODULE = require('./TradingRecords.js')
|
||||
let tradingRecordsModule = TRADING_RECORDS_MODULE.newTradingRecords(bot, logger)
|
||||
tradingRecordsModule.initialize(outputDatasetsMap)
|
||||
|
||||
const TRADING_SYSTEM_MODULE = require('./TradingSystem.js')
|
||||
let tradingSystemModule = TRADING_SYSTEM_MODULE.newTradingSystem(bot, logger, tradingEngineModule)
|
||||
tradingSystemModule.initialize()
|
||||
|
||||
const TRADING_EPISODE_MODULE = require('./TradingEpisode.js')
|
||||
let tradingEpisodeModule = TRADING_EPISODE_MODULE.newTradingEpisode(bot, logger, tradingEngineModule)
|
||||
tradingEpisodeModule.initialize()
|
||||
|
||||
/* Setting up the candles array: The whole simulation is based on the array of candles at the time-frame defined at the session parameters. */
|
||||
let propertyName = 'at' + sessionParameters.timeFrame.config.label.replace('-', '')
|
||||
let candles = chart[propertyName].candles
|
||||
|
||||
/* Variables needed for heartbeat functionality */
|
||||
let heartBeatDate
|
||||
let previousHeartBeatDate
|
||||
/*
|
||||
Estimation of the Initial Candle to Process in this Run.
|
||||
*/
|
||||
let initialCandle
|
||||
if (bot.FIRST_EXECUTION === true && bot.RESUME === false) {
|
||||
/* Estimate Initial Candle based on the timeRage configured for the session. */
|
||||
let firstEnd = candles[0].end
|
||||
let targetEnd = sessionParameters.timeRange.config.initialDatetime
|
||||
let diff = targetEnd - firstEnd
|
||||
let amount = diff / sessionParameters.timeFrame.config.value
|
||||
|
||||
initialCandle = Math.trunc(amount)
|
||||
if (initialCandle < 0) { initialCandle = 0 }
|
||||
if (initialCandle > candles.length - 1) {
|
||||
/*
|
||||
This will happen when the sessionParameters.timeRange.config.initialDatetime is beyond the last candle available,
|
||||
meaning that the dataSet needs to be updated with more up-to-date data.
|
||||
*/
|
||||
bot.TRADING_SESSION.stop('Data is not up-to-date enough. Please start the Masters Data Mining Operation.')
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[IMPORTANT] runSimulation -> Data is not up-to-date enough. Stopping the Session now. ') }
|
||||
return
|
||||
}
|
||||
} else {
|
||||
|
||||
/*
|
||||
In this case we already have at the last candle index the next candle to be
|
||||
processed. We will just continue with this candle.
|
||||
*/
|
||||
initialCandle = tradingEngine.current.episode.candle.index.value
|
||||
}
|
||||
/*
|
||||
Main Simulation Loop
|
||||
*/
|
||||
/* We are going to use this to exit the loop if needed. */
|
||||
let breakLoop = false
|
||||
|
||||
/*
|
||||
We will assume that we are at the head of the market here. We do this
|
||||
because the loop could be empty and no validation is going to run. If the
|
||||
loop is not empty, then the lascCandle() check will override this value
|
||||
depending on if we really are at the head of the market or not.
|
||||
*/
|
||||
tradingEngine.current.episode.headOfTheMarket.value = true
|
||||
/*
|
||||
This is the main simulation loop. It will go through the initial candle
|
||||
until one less than the last candle available. We will never process the last
|
||||
candle available since it is not considered a closed candle, but a candle
|
||||
that still can change. So effectively will be processing all closed candles.
|
||||
*/
|
||||
for (let i = initialCandle; i < candles.length - 1; i++) {
|
||||
tradingEngine.current.episode.candle.index.value = i
|
||||
|
||||
/* This is the current candle the Simulation is working at. */
|
||||
let candle = candles[tradingEngine.current.episode.candle.index.value]
|
||||
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> Candle Begin @ ' + (new Date(candle.begin)).toLocaleString()) }
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> Candle End @ ' + (new Date(candle.end)).toLocaleString()) }
|
||||
|
||||
tradingEngineModule.setCurrentCandle(candle) // We move the current candle we are standing at, to the trading engine data structure to make it available to anyone, including conditions and formulas.
|
||||
|
||||
/* We emit a heart beat so that the UI can now where we are at the overal process.*/
|
||||
heartBeat()
|
||||
|
||||
/* Opening the Episode, if needed. */
|
||||
tradingEpisodeModule.openEpisode()
|
||||
|
||||
if (checkInitialDatetime() === false) {
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> Candle Before the Initia Date Time @ ' + (new Date(candle.begin)).toLocaleString()) }
|
||||
continue
|
||||
}
|
||||
|
||||
positionChartAtCurrentCandle()
|
||||
|
||||
/* The chart was recalculated based on the current candle. */
|
||||
tradingSystemModule.updateChart(chart)
|
||||
|
||||
/*
|
||||
Do the stuff needed previous to the run like
|
||||
Episode Counters and Statistics update. Mantaince is done
|
||||
once per simulation candle.
|
||||
*/
|
||||
tradingSystemModule.mantain()
|
||||
tradingEpisodeModule.mantain()
|
||||
tradingEngineModule.mantain()
|
||||
|
||||
/*
|
||||
Run the first cycle of the Trading System. In this first cycle we
|
||||
give some room so that orders can be canceled or filled and we can
|
||||
write those records into the output memory. During this cycle new
|
||||
orders can not be created, since otherwise the could be cancelled at
|
||||
the second cycle without spending real time at the order book.
|
||||
*/
|
||||
tradingEngineModule.setCurrentCycle('First')
|
||||
await runCycle()
|
||||
|
||||
/*
|
||||
We check if we need to stop before appending the records so that the stop
|
||||
reason is also propery recorded. Note also that we check this after the first
|
||||
cycle, where orders have not been submitted to the exchange yet, but we
|
||||
had the chance to check for the status of placed orders or even cancel
|
||||
the ones that needed cancellation.
|
||||
*/
|
||||
checkIfWeNeedToStopBetweenCycles()
|
||||
|
||||
/* Add new records to the process output */
|
||||
tradingRecordsModule.appendRecords()
|
||||
|
||||
if (breakLoop === true) { break }
|
||||
/*
|
||||
Run the second cycle of the Trading System. During this second run
|
||||
some new orders might be created at slots freed up during the first
|
||||
run. This allows for example for a Limit Order to be cancelled during the
|
||||
first run, and the same Limit Order definition to spawn a new order
|
||||
without the need to wait until the next candle. Orders can not be cancelled
|
||||
during the second cycle.
|
||||
*/
|
||||
tradingEngineModule.setCurrentCycle('Second')
|
||||
await runCycle()
|
||||
|
||||
checkIfWeNeedToStopAfterBothCycles()
|
||||
|
||||
/* Add new records to the process output */
|
||||
tradingRecordsModule.appendRecords()
|
||||
|
||||
if (breakLoop === true) { break }
|
||||
|
||||
async function runCycle() {
|
||||
/* Reset Data Structures */
|
||||
tradingSystemModule.reset()
|
||||
tradingEpisodeModule.reset()
|
||||
tradingEngineModule.reset()
|
||||
|
||||
let infoMessage = 'Processing candle # ' + tradingEngine.current.episode.candle.index.value + ' @ the ' + tradingEngine.current.episode.cycle.value + ' cycle.'
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> ' + infoMessage) }
|
||||
tradingSystem.infos.push([tradingSystem.id, infoMessage])
|
||||
|
||||
await tradingSystemModule.run()
|
||||
}
|
||||
|
||||
function checkIfWeNeedToStopBetweenCycles() {
|
||||
if (bot.STOP_SESSION === true) {
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> controlLoop -> We are going to stop here bacause we were requested to stop processing this session.') }
|
||||
updateEpisode('Session Stopped')
|
||||
breakLoop = true
|
||||
return
|
||||
}
|
||||
|
||||
if (global.STOP_TASK_GRACEFULLY === true) {
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> controlLoop -> We are going to stop here bacause we were requested to stop processing this task.') }
|
||||
updateEpisode('Task Stopped')
|
||||
breakLoop = true
|
||||
return
|
||||
}
|
||||
|
||||
if (checkFinalDatetime() === false) {
|
||||
closeEpisode('Final Datetime Reached')
|
||||
breakLoop = true
|
||||
bot.TRADING_SESSION.stop('Final Datetime Reached')
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[IMPORTANT] runSimulation -> Final Datetime Reached. Stopping the Session now. ') }
|
||||
return
|
||||
}
|
||||
|
||||
if (checkMinimunAndMaximunBalance() === false) {
|
||||
closeEpisode('Min or Max Balance Reached')
|
||||
breakLoop = true
|
||||
bot.TRADING_SESSION.stop('Min or Max Balance Reached')
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[IMPORTANT] runSimulation -> Min or Max Balance Reached. Stopping the Session now. ') }
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function checkIfWeNeedToStopAfterBothCycles() {
|
||||
if (checkNextCandle() === false) {
|
||||
updateEpisode('All Candles Processed')
|
||||
breakLoop = true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tradingSystemModule.finalize()
|
||||
tradingRecordsModule.finalize()
|
||||
tradingEpisodeModule.finalize()
|
||||
|
||||
tradingSystemModule = undefined
|
||||
tradingRecordsModule = undefined
|
||||
tradingEpisodeModule = undefined
|
||||
|
||||
await writeFiles()
|
||||
|
||||
function closeEpisode(exitType) {
|
||||
tradingEpisodeModule.updateExitType(exitType)
|
||||
tradingEpisodeModule.closeEpisode()
|
||||
}
|
||||
|
||||
function updateEpisode(exitType) {
|
||||
tradingEpisodeModule.updateExitType(exitType)
|
||||
}
|
||||
|
||||
function heartBeat() {
|
||||
let hartbeatText = ''
|
||||
if (sessionParameters.heartbeats !== undefined) {
|
||||
if (sessionParameters.heartbeats.config.date === true || sessionParameters.heartbeats.config.candleIndex === true) {
|
||||
/* We will produce a simulation level heartbeat in order to inform the user this is running. */
|
||||
|
||||
heartBeatDate = new Date(Math.trunc(tradingEngine.current.episode.candle.begin.value / global.ONE_DAY_IN_MILISECONDS) * global.ONE_DAY_IN_MILISECONDS)
|
||||
|
||||
let fromDate = new Date(sessionParameters.timeRange.config.initialDatetime)
|
||||
let lastDate = new Date(sessionParameters.timeRange.config.finalDatetime)
|
||||
|
||||
let currentDateString = heartBeatDate.getUTCFullYear() + '-' + utilities.pad(heartBeatDate.getUTCMonth() + 1, 2) + '-' + utilities.pad(heartBeatDate.getUTCDate(), 2)
|
||||
let currentDate = new Date(heartBeatDate)
|
||||
let percentage = global.getPercentage(fromDate, currentDate, lastDate)
|
||||
|
||||
/*
|
||||
Theere are a few tasks that we need to do only when the date changes,
|
||||
otherwise it would be suboptimal.
|
||||
*/
|
||||
if (heartBeatDate.valueOf() !== previousHeartBeatDate) {
|
||||
previousHeartBeatDate = heartBeatDate.valueOf()
|
||||
|
||||
let processingDate = heartBeatDate.getUTCFullYear() + '-' + utilities.pad(heartBeatDate.getUTCMonth() + 1, 2) + '-' + utilities.pad(heartBeatDate.getUTCDate(), 2)
|
||||
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> Simulation ' + bot.TRADING_SESSIONKey + ' Loop # ' + tradingEngine.current.episode.candle.index.value + ' @ ' + processingDate) }
|
||||
|
||||
/* Logging to console and disk */
|
||||
if (global.areEqualDates(currentDate, new Date()) === false) {
|
||||
logger.newInternalLoop(bot.codeName, bot.process, currentDate, percentage)
|
||||
}
|
||||
|
||||
/* Date only hearbeat */
|
||||
if (sessionParameters.heartbeats.config.date === true && sessionParameters.heartbeats.config.candleIndex === false) {
|
||||
hartbeatText = hartbeatText + currentDateString
|
||||
bot.processHeartBeat(hartbeatText, percentage)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
When the Candle Index nees to be shown, then we can not send the hearbet
|
||||
only when the dates changes, we have to send it for every candle.
|
||||
It might also contain the date information.
|
||||
*/
|
||||
if (sessionParameters.heartbeats.config.candleIndex === true) {
|
||||
if (sessionParameters.heartbeats.config.date === true) {
|
||||
hartbeatText = hartbeatText + currentDateString
|
||||
}
|
||||
hartbeatText = hartbeatText + ' Candle # ' + tradingEngine.current.episode.candle.index.value
|
||||
bot.processHeartBeat(hartbeatText, percentage)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function positionChartAtCurrentCandle() {
|
||||
/*
|
||||
In conditions and Formulas, we want users to have an easy sintax to refer to indicators. In order to achieve that, we need the user to have
|
||||
easy access to the current candle for instance, or the current bollinger band, meaning the one the Simulation is currently standing at.
|
||||
For that reason we do the following processing, to have at the chart data structure the current objects of each indicator / time frame.
|
||||
*/
|
||||
let dataDependencies = global.NODE_BRANCH_TO_ARRAY(bot.processNode.referenceParent.processDependencies, 'Data Dependency')
|
||||
dataDependencies = global.FILTER_OUT_NODES_WITHOUT_REFERENCE_PARENT_FROM_NODE_ARRAY(dataDependencies)
|
||||
|
||||
/* Finding the Current Element on Market Files */
|
||||
if (bot.processingDailyFiles) {
|
||||
for (let j = 0; j < global.dailyFilePeriods.length; j++) {
|
||||
let mapKey = dailyFilePeriods[j][1]
|
||||
let propertyName = 'at' + mapKey.replace('-', '')
|
||||
let thisChart = chart[propertyName]
|
||||
|
||||
for (let k = 0; k < dataDependencies.length; k++) {
|
||||
let dataDependencyNode = dataDependencies[k]
|
||||
if (dataDependencyNode.referenceParent.config.codeName !== 'Multi-Period-Daily') { continue }
|
||||
let singularVariableName = dataDependencyNode.referenceParent.parentNode.config.singularVariableName
|
||||
let pluralVariableName = dataDependencyNode.referenceParent.parentNode.config.pluralVariableName
|
||||
if (thisChart[pluralVariableName] !== undefined) {
|
||||
let currentElement = getElement(thisChart[pluralVariableName], 'Daily' + '-' + mapKey + '-' + pluralVariableName)
|
||||
if (currentElement !== undefined) {
|
||||
thisChart[singularVariableName] = currentElement
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Finding the Current Element on Market Files */
|
||||
for (let j = 0; j < global.marketFilesPeriods.length; j++) {
|
||||
let mapKey = marketFilesPeriods[j][1]
|
||||
let propertyName = 'at' + mapKey.replace('-', '')
|
||||
let thisChart = chart[propertyName]
|
||||
|
||||
for (let k = 0; k < dataDependencies.length; k++) {
|
||||
let dataDependencyNode = dataDependencies[k]
|
||||
if (dataDependencyNode.referenceParent.config.codeName !== 'Multi-Period-Market') { continue }
|
||||
let singularVariableName = dataDependencyNode.referenceParent.parentNode.config.singularVariableName
|
||||
let pluralVariableName = dataDependencyNode.referenceParent.parentNode.config.pluralVariableName
|
||||
if (thisChart[pluralVariableName] !== undefined) {
|
||||
let currentElement = getElement(thisChart[pluralVariableName], 'Market' + '-' + mapKey + '-' + pluralVariableName)
|
||||
if (currentElement !== undefined) {
|
||||
thisChart[singularVariableName] = currentElement
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Finding the Current Element At Single Files*/
|
||||
let propertyName = 'atAnyTimeFrame'
|
||||
let thisChart = chart[propertyName]
|
||||
|
||||
for (let k = 0; k < dataDependencies.length; k++) {
|
||||
let dataDependencyNode = dataDependencies[k]
|
||||
if (dataDependencyNode.referenceParent.config.codeName !== 'Single-File') { continue }
|
||||
let singularVariableName = dataDependencyNode.referenceParent.parentNode.config.singularVariableName
|
||||
let pluralVariableName = dataDependencyNode.referenceParent.parentNode.config.pluralVariableName
|
||||
let elementArray = thisChart[pluralVariableName]
|
||||
let currentElement
|
||||
if (elementArray !== undefined) {
|
||||
currentElement = elementArray[elementArray.length - 1]
|
||||
}
|
||||
thisChart[singularVariableName] = currentElement
|
||||
}
|
||||
}
|
||||
|
||||
function getElement(pArray, datasetName) {
|
||||
if (pArray === undefined) { return }
|
||||
try {
|
||||
let element
|
||||
for (let i = 0; i < pArray.length; i++) {
|
||||
element = pArray[i]
|
||||
|
||||
if (tradingEngine.current.episode.candle.end.value === element.end) { // when there is an exact match at the end we take that element
|
||||
return element
|
||||
} else {
|
||||
if (
|
||||
i > 0 &&
|
||||
element.end > tradingEngine.current.episode.candle.end.value
|
||||
) {
|
||||
let previousElement = pArray[i - 1]
|
||||
if (previousElement.end < tradingEngine.current.episode.candle.end.value) {
|
||||
return previousElement // If one elements goes into the future of currentCandle, then we stop and take the previous element.
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, '[ERROR] runSimulation -> getElement -> datasetName = ' + datasetName)
|
||||
logger.write(MODULE_NAME, '[ERROR] runSimulation -> getElement -> err = ' + err.stack)
|
||||
throw (global.DEFAULT_FAIL_RESPONSE)
|
||||
}
|
||||
}
|
||||
|
||||
function checkNextCandle() {
|
||||
/*
|
||||
We need to check that the candle we have just processed it is not the last candle.
|
||||
The candle at the head of the market is already skipped from the loop because it has not closed yet.
|
||||
Note: for Daily Files, this means that the last candle of each day will never be processed.
|
||||
|
||||
The first +1 is because array indexes are based on 0.
|
||||
The second +1 is because we need to compare the next candle (remember that the loops allways avoid the
|
||||
last candle of the dataset available.)
|
||||
*/
|
||||
if (tradingEngine.current.episode.candle.index.value + 1 + 1 === candles.length) {
|
||||
/*
|
||||
When processing daily files, we need a mechanism to turn from one day to the next one.
|
||||
That mechanism is the one implemented here. If we detect that the next candle is the last candle of
|
||||
the day, we will advance current process day one day. By doing so, during the next execution, the
|
||||
simulation will receive the candles and indicators files of the next day.
|
||||
*/
|
||||
let candlesPerDay = global.ONE_DAY_IN_MILISECONDS / sessionParameters.timeFrame.config.value
|
||||
if (
|
||||
bot.processingDailyFiles &&
|
||||
tradingEngine.current.episode.candle.index.value + 1 + 1 === candlesPerDay
|
||||
) {
|
||||
/*
|
||||
Here we found that the next candle of the dataset is the last candle of the day.
|
||||
It is time to move to the next day so as to receive at the next execution, the indicator files from
|
||||
the next day. At the same time we will reset the index to be pointing to
|
||||
the first candle of the new dataset we shall receive. The first candle of the next day starts
|
||||
at index 0, so we will position the index now at zero.
|
||||
*/
|
||||
tradingEngine.current.episode.candle.index.value = 0
|
||||
tradingEngine.current.episode.processDate.value =
|
||||
tradingEngine.current.episode.processDate.value + global.ONE_DAY_IN_MILISECONDS
|
||||
return false
|
||||
}
|
||||
|
||||
/*
|
||||
We reached the head of the market but we are not at the last candle of a day during
|
||||
Daily Files processing. We will advance to the next candle index anyways because in the
|
||||
next execution it will likely have more candles at the dataset. And if it does not,
|
||||
it will just wait there until it does.
|
||||
*/
|
||||
tradingEngine.current.episode.headOfTheMarket.value = true
|
||||
tradingEngine.current.episode.candle.index.value++
|
||||
return false
|
||||
} else {
|
||||
|
||||
/* Wd did not reach the head of the market */
|
||||
tradingEngine.current.episode.headOfTheMarket.value = false
|
||||
tradingEngine.current.episode.candle.index.value++
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
function checkInitialDatetime() {
|
||||
/* Here we check that the current candle is not before the initial datetime defined at the session parameters.*/
|
||||
if (tradingEngine.current.episode.candle.end.value < sessionParameters.timeRange.config.initialDatetime) {
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> checkInitialAndFinalDatetime -> Skipping Candle before the sessionParameters.timeRange.config.initialDatetime.') }
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function checkFinalDatetime() {
|
||||
/* Here we check that the next candle is not after of the user-defined final datetime at the session parameters.*/
|
||||
if (tradingEngine.current.episode.candle.begin.value + sessionParameters.timeFrame.config.value > sessionParameters.timeRange.config.finalDatetime) {
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> checkInitialAndFinalDatetime -> Skipping Candle after the sessionParameters.timeRange.config.finalDatetime.') }
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function checkMinimunAndMaximunBalance() {
|
||||
/* Checks for Minimun and Maximun Balance. We do the check while not inside any strategy only. */
|
||||
if (
|
||||
tradingEngine.current.strategy.index.value === tradingEngine.current.strategy.index.config.initialValue
|
||||
) {
|
||||
/*
|
||||
We will perform this check only when we are not inside a position,
|
||||
because there the balances have shifted from their resting position.
|
||||
*/
|
||||
|
||||
let stopRunningDate = (new Date(tradingEngine.current.episode.candle.begin.value)).toLocaleString()
|
||||
|
||||
if (sessionParameters.sessionBaseAsset.config.minimumBalance !== undefined) {
|
||||
if (tradingEngine.current.episode.episodeBaseAsset.balance.value <= sessionParameters.sessionBaseAsset.config.minimumBalance) {
|
||||
const errorMessage = 'Min Balance reached @ ' + stopRunningDate
|
||||
tradingSystem.errors.push([tradingSystem.id, errorMessage])
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[WARN] runSimulation -> checkMinimunAndMaximunBalance -> ' + errorMessage) }
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if (sessionParameters.sessionBaseAsset.config.maximumBalance !== undefined) {
|
||||
if (tradingEngine.current.episode.episodeBaseAsset.balance.value >= sessionParameters.sessionBaseAsset.config.maximumBalance) {
|
||||
const errorMessage = 'Max Balance reached @ ' + stopRunningDate
|
||||
tradingSystem.errors.push([tradingSystem.id, errorMessage])
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[WARN] runSimulation -> checkMinimunAndMaximunBalance -> ' + errorMessage) }
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if (sessionParameters.sessionQuotedAsset.config.minimumBalance !== undefined) {
|
||||
if (tradingEngine.current.episode.episodeQuotedAsset.balance.value <= sessionParameters.sessionQuotedAsset.config.minimumBalance) {
|
||||
const errorMessage = 'Min Balance reached @ ' + stopRunningDate
|
||||
tradingSystem.errors.push([tradingSystem.id, errorMessage])
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[WARN] runSimulation -> checkMinimunAndMaximunBalance -> ' + errorMessage) }
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if (sessionParameters.sessionQuotedAsset.config.maximumBalance !== undefined) {
|
||||
if (tradingEngine.current.episode.episodeQuotedAsset.balance.value >= sessionParameters.sessionQuotedAsset.config.maximumBalance) {
|
||||
const errorMessage = 'Max Balance reached @ ' + stopRunningDate
|
||||
tradingSystem.errors.push([tradingSystem.id, errorMessage])
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[WARN] runSimulation -> checkMinimunAndMaximunBalance -> ' + errorMessage) }
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return true
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, '[ERROR] runSimulation -> err = ' + err.stack)
|
||||
throw (global.DEFAULT_FAIL_RESPONSE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,87 @@
|
||||
exports.newTradingStrategy = function newTradingStrategy(bot, logger, tradingEngineModule) {
|
||||
/*
|
||||
This module packages all functions related to Strategies.
|
||||
*/
|
||||
const MODULE_NAME = 'Trading Strategy'
|
||||
let thisObject = {
|
||||
mantain: mantain,
|
||||
reset: reset,
|
||||
openStrategy: openStrategy,
|
||||
closeStrategy: closeStrategy,
|
||||
initialize: initialize,
|
||||
finalize: finalize
|
||||
}
|
||||
|
||||
let tradingEngine
|
||||
let sessionParameters
|
||||
|
||||
return thisObject
|
||||
|
||||
function initialize() {
|
||||
tradingEngine = bot.simulationState.tradingEngine
|
||||
sessionParameters = bot.TRADING_SESSION.tradingParameters
|
||||
}
|
||||
|
||||
function finalize() {
|
||||
tradingEngine = undefined
|
||||
sessionParameters = undefined
|
||||
}
|
||||
|
||||
function mantain() {
|
||||
updateCounters()
|
||||
updateEnds()
|
||||
}
|
||||
|
||||
function reset() {
|
||||
resetTradingEngineDataStructure()
|
||||
}
|
||||
|
||||
function openStrategy(index, situationName, strategyName) {
|
||||
/* Starting begin and end */
|
||||
tradingEngine.current.strategy.begin.value = tradingEngine.current.episode.cycle.lastBegin.value
|
||||
tradingEngine.current.strategy.end.value = tradingEngine.current.episode.cycle.lastEnd.value
|
||||
|
||||
/* Recording the opening at the Trading Engine Data Structure */
|
||||
tradingEngine.current.strategy.status.value = 'Open'
|
||||
tradingEngine.current.strategy.serialNumber.value = tradingEngine.current.episode.episodeCounters.strategies.value
|
||||
tradingEngine.current.strategy.identifier.value = global.UNIQUE_ID()
|
||||
tradingEngine.current.strategy.beginRate.value = tradingEngine.current.episode.candle.min.value
|
||||
|
||||
tradingEngine.current.strategy.index.value = index
|
||||
tradingEngine.current.strategy.situationName.value = situationName
|
||||
tradingEngine.current.strategy.strategyName.value = strategyName
|
||||
|
||||
/* Updating Episode Counters */
|
||||
tradingEngine.current.episode.episodeCounters.strategies.value++
|
||||
}
|
||||
|
||||
function closeStrategy(exitType) {
|
||||
tradingEngine.current.strategy.status.value = 'Closed'
|
||||
tradingEngine.current.strategy.exitType.value = exitType
|
||||
tradingEngine.current.strategy.end.value = tradingEngine.current.episode.cycle.lastEnd.value
|
||||
tradingEngine.current.strategy.endRate.value = tradingEngine.current.episode.candle.min.value
|
||||
/*
|
||||
Now that the strategy is closed, it is the right time to move this strategy from current to last at the Trading Engine data structure.
|
||||
*/
|
||||
tradingEngineModule.cloneValues(tradingEngine.current.strategy, tradingEngine.last.strategy)
|
||||
}
|
||||
|
||||
function updateEnds() {
|
||||
if (tradingEngine.current.strategy.status.value === 'Open') {
|
||||
tradingEngine.current.strategy.end.value = tradingEngine.current.strategy.end.value + sessionParameters.timeFrame.config.value
|
||||
tradingEngine.current.strategy.endRate.value = tradingEngine.current.episode.candle.close.value
|
||||
}
|
||||
}
|
||||
|
||||
function resetTradingEngineDataStructure() {
|
||||
if (tradingEngine.current.strategy.status.value === 'Closed') {
|
||||
tradingEngineModule.initializeNode(tradingEngine.current.strategy)
|
||||
}
|
||||
}
|
||||
|
||||
function updateCounters() {
|
||||
if (tradingEngine.current.strategy.status.value === 'Open') {
|
||||
tradingEngine.current.strategy.strategyCounters.periods.value++
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
exports.newTradingSystem = function newTradingSystem(bot, logger, tradingEngineModule) {
|
||||
/*
|
||||
The Trading System is the user defined set of rules compliant with the Trading Protocol that
|
||||
defines the trading logic to be applied during each cycle of the Simulation.
|
||||
*/
|
||||
const MODULE_NAME = 'Trading System'
|
||||
let thisObject = {
|
||||
mantain: mantain,
|
||||
reset: reset,
|
||||
run: run,
|
||||
updateChart: updateChart,
|
||||
initialize: initialize,
|
||||
finalize: finalize
|
||||
}
|
||||
|
||||
let chart
|
||||
let tradingSystem
|
||||
let tradingEngine
|
||||
let sessionParameters
|
||||
let dynamicIndicators
|
||||
|
||||
const TRADING_STAGES_MODULE = require('./TradingStages.js')
|
||||
let tradingStagesModule = TRADING_STAGES_MODULE.newTradingStages(bot, logger, tradingEngineModule)
|
||||
|
||||
|
||||
return thisObject
|
||||
|
||||
function initialize() {
|
||||
tradingSystem = bot.simulationState.tradingSystem
|
||||
tradingEngine = bot.simulationState.tradingEngine
|
||||
sessionParameters = bot.TRADING_SESSION.tradingParameters
|
||||
|
||||
tradingSystem.conditions = new Map()
|
||||
tradingSystem.formulas = new Map()
|
||||
|
||||
tradingStagesModule.initialize()
|
||||
|
||||
/* Adding Functions used elsewhere to Trading System Definition */
|
||||
tradingSystem.checkConditions = function (situation, passed) {
|
||||
/* Given a Situation, we check if all children conditions are true or not */
|
||||
for (let m = 0; m < situation.conditions.length; m++) {
|
||||
|
||||
let condition = situation.conditions[m]
|
||||
let value = false
|
||||
if (tradingSystem.conditions.get(condition.id) !== undefined) {
|
||||
value = tradingSystem.conditions.get(condition.id)
|
||||
}
|
||||
if (value === true) {
|
||||
tradingSystem.highlights.push(condition.id)
|
||||
}
|
||||
else {
|
||||
passed = false
|
||||
}
|
||||
tradingSystem.values.push([condition.id, value])
|
||||
}
|
||||
return passed
|
||||
}
|
||||
|
||||
tradingSystem.evalConditions = function (startingNode, descendentOfNodeType) {
|
||||
evalNode(startingNode, 'Conditions', descendentOfNodeType)
|
||||
}
|
||||
|
||||
tradingSystem.evalFormulas = function (startingNode, descendentOfNodeType) {
|
||||
evalNode(startingNode, 'Formulas', descendentOfNodeType)
|
||||
}
|
||||
|
||||
function isItInside(elementWithTimestamp, elementWithBeginEnd) {
|
||||
/*
|
||||
The function is to allow in Conditions and Formulas to easily know when an element with timestamp (like the ones of single files) are inside the time range
|
||||
of an element with a time range, like a candle.
|
||||
*/
|
||||
if (elementWithTimestamp.timestamp >= elementWithBeginEnd.begin && elementWithTimestamp.timestamp <= elementWithBeginEnd.end) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function finalize() {
|
||||
tradingStagesModule.finalize()
|
||||
tradingStagesModule = undefined
|
||||
|
||||
chart = undefined
|
||||
|
||||
tradingSystem.conditions = undefined
|
||||
tradingSystem.formulas = undefined
|
||||
|
||||
tradingSystem.highlights = undefined
|
||||
tradingSystem.errors = undefined
|
||||
tradingSystem.warnings = undefined
|
||||
tradingSystem.infos = undefined
|
||||
tradingSystem.values = undefined
|
||||
tradingSystem.status = undefined
|
||||
tradingSystem.progress = undefined
|
||||
tradingSystem.running = undefined
|
||||
|
||||
tradingSystem = undefined
|
||||
tradingEngine = undefined
|
||||
sessionParameters = undefined
|
||||
}
|
||||
|
||||
function mantain() {
|
||||
tradingStagesModule.mantain()
|
||||
}
|
||||
|
||||
function reset() {
|
||||
tradingStagesModule.reset()
|
||||
|
||||
tradingSystem.highlights = []
|
||||
tradingSystem.errors = []
|
||||
tradingSystem.warnings = []
|
||||
tradingSystem.infos = []
|
||||
tradingSystem.values = []
|
||||
tradingSystem.status = []
|
||||
tradingSystem.progress = []
|
||||
tradingSystem.running = []
|
||||
tradingSystem.announcements = []
|
||||
}
|
||||
|
||||
function updateChart(pChart) {
|
||||
chart = pChart // We need chat to be a local object accessible from conditions and formulas.
|
||||
tradingStagesModule.updateChart(pChart)
|
||||
}
|
||||
|
||||
function buildDynamicIndicators() {
|
||||
if (tradingSystem.dynamicIndicators !== undefined) {
|
||||
dynamicIndicators = {}
|
||||
/* Eval Dynamic Indicators */
|
||||
tradingSystem.evalFormulas(tradingSystem.dynamicIndicators, 'Indicator Function')
|
||||
|
||||
for (let i = 0; i < tradingSystem.dynamicIndicators.indicatorFunctions.length; i++) {
|
||||
let indicatorFunction = tradingSystem.dynamicIndicators.indicatorFunctions[i]
|
||||
if (indicatorFunction.formula === undefined) { return }
|
||||
if (indicatorFunction.config.codeName === undefined) { return }
|
||||
dynamicIndicators[indicatorFunction.config.codeName] = tradingSystem.formulas.get(indicatorFunction.formula.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
/* Dynamic Indicators */
|
||||
buildDynamicIndicators()
|
||||
|
||||
/* Run the Trigger Stage */
|
||||
tradingStagesModule.runTriggerStage()
|
||||
|
||||
/* Run the Open Stage */
|
||||
await tradingStagesModule.runOpenStage()
|
||||
|
||||
/* Run the Manage Stage */
|
||||
tradingStagesModule.runManageStage()
|
||||
|
||||
/* Run the Close Stage */
|
||||
await tradingStagesModule.runCloseStage()
|
||||
|
||||
tradingStagesModule.cycleBasedStatistics()
|
||||
|
||||
} catch (err) {
|
||||
/*
|
||||
If an error ocurred during execution, it was alreeady logged and
|
||||
included at the errors array. That means we need to do nothing here,
|
||||
just prevent the execution to be halted for not handling exceptions.
|
||||
*/
|
||||
if (typeof err === 'string' || err instanceof String) {
|
||||
logger.write(MODULE_NAME, '[ERROR] runExecution -> err = ' + err)
|
||||
}
|
||||
if (err.stack !== undefined) {
|
||||
logger.write(MODULE_NAME, '[ERROR] runExecution -> err = ' + err.stack)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function evalNode(node, evaluating, descendentOfNodeType, isDescendent) {
|
||||
if (node === undefined) { return }
|
||||
|
||||
/* Verify if this node is decendent of the specified node type */
|
||||
if (isDescendent !== true) {
|
||||
if (node.type === descendentOfNodeType) {
|
||||
isDescendent = true
|
||||
}
|
||||
}
|
||||
|
||||
/* Here we check if there is a codition to be evaluated */
|
||||
if (node.type === 'Condition' && evaluating === 'Conditions') {
|
||||
/* We will eval this condition */
|
||||
if (isDescendent === true) {
|
||||
evalCondition(node)
|
||||
}
|
||||
}
|
||||
|
||||
/* Here we check if there is a formula to be evaluated */
|
||||
if ((node.type === 'Formula' || node.type === 'Announcement Formula') && evaluating === 'Formulas') {
|
||||
if (node.code !== undefined) {
|
||||
/* We will eval this formula */
|
||||
if (isDescendent === true) {
|
||||
evalFormula(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Now we go down through all this node children */
|
||||
let nodeDefinition = global.APP_SCHEMA_MAP.get(node.type)
|
||||
if (nodeDefinition === undefined) { return }
|
||||
|
||||
if (nodeDefinition.properties !== undefined) {
|
||||
let previousPropertyName // Since there are cases where there are many properties with the same name,because they can hold nodes of different types but only one at the time, we have to avoind counting each property of those as individual children.
|
||||
for (let i = 0; i < nodeDefinition.properties.length; i++) {
|
||||
let property = nodeDefinition.properties[i]
|
||||
|
||||
switch (property.type) {
|
||||
case 'node': {
|
||||
if (property.name !== previousPropertyName) {
|
||||
if (node[property.name] !== undefined) {
|
||||
evalNode(node[property.name], evaluating, descendentOfNodeType, isDescendent)
|
||||
}
|
||||
previousPropertyName = property.name
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'array': {
|
||||
if (node[property.name] !== undefined) {
|
||||
let nodePropertyArray = node[property.name]
|
||||
for (let m = 0; m < nodePropertyArray.length; m++) {
|
||||
evalNode(nodePropertyArray[m], evaluating, descendentOfNodeType, isDescendent)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function evalCondition(node) {
|
||||
let value
|
||||
let error
|
||||
/*
|
||||
The code can be at the condition node if it was done with the Conditions Editor, or it can also be
|
||||
at a Javascript Code node. If there is a Javascript object we will give it preference and take the code
|
||||
from there. Otherwise we will take the code from the Condition node.
|
||||
*/
|
||||
let code = node.code
|
||||
if (node.javascriptCode !== undefined) {
|
||||
if (node.javascriptCode.code !== undefined) {
|
||||
code = node.javascriptCode.code
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
logger.write(MODULE_NAME, '[INFO] evalCondition -> ' + node.name + ' -> code = ' + code)
|
||||
value = eval(code)
|
||||
} catch (err) {
|
||||
/*
|
||||
One possible error is that the conditions references a .previous that is undefined. This
|
||||
will not be considered an error.
|
||||
*/
|
||||
value = false
|
||||
error = err.message
|
||||
}
|
||||
|
||||
tradingSystem.conditions.set(node.id, value)
|
||||
|
||||
if (value === true) {
|
||||
tradingSystem.highlights.push(node.id)
|
||||
}
|
||||
if (error !== undefined) {
|
||||
tradingSystem.errors.push([node.id, error])
|
||||
}
|
||||
if (value !== undefined) {
|
||||
tradingSystem.values.push([node.id, value])
|
||||
}
|
||||
|
||||
logger.write(MODULE_NAME, '[INFO] evalCondition -> value = ' + value)
|
||||
logger.write(MODULE_NAME, '[INFO] evalCondition -> error = ' + error)
|
||||
}
|
||||
|
||||
function evalFormula(node) {
|
||||
let value
|
||||
let error
|
||||
|
||||
try {
|
||||
logger.write(MODULE_NAME, '[INFO] evalFormula -> ' + node.name + ' -> code = ' + node.code)
|
||||
value = eval(node.code)
|
||||
} catch (err) {
|
||||
/*
|
||||
One possible error is that the formula references a .previous that is undefined. This
|
||||
will not be considered an error.
|
||||
*/
|
||||
value = 0
|
||||
error = err.message
|
||||
}
|
||||
|
||||
if (error !== undefined) {
|
||||
tradingSystem.errors.push([node.id, error])
|
||||
logger.write(MODULE_NAME, '[INFO] evalFormula -> error = ' + error)
|
||||
return
|
||||
}
|
||||
if (value !== undefined) {
|
||||
if (node.type === 'Formula' && isNaN(value)) {
|
||||
tradingSystem.errors.push([node.id, 'Formula needs to return a number.'])
|
||||
return
|
||||
}
|
||||
|
||||
tradingSystem.values.push([node.id, value])
|
||||
tradingSystem.formulas.set(node.id, value)
|
||||
}
|
||||
|
||||
logger.write(MODULE_NAME, '[INFO] evalFormula -> value = ' + value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,570 @@
|
||||
exports.newMultiPeriodDaily = function newMultiPeriodDaily(bot, logger, UTILITIES, FILE_STORAGE) {
|
||||
const FULL_LOG = true;
|
||||
const MODULE_NAME = "Multi Period Daily";
|
||||
const GMT_SECONDS = ':00.000 GMT+0000';
|
||||
|
||||
thisObject = {
|
||||
initialize: initialize,
|
||||
finalize: finalize,
|
||||
start: start
|
||||
};
|
||||
|
||||
let utilities = UTILITIES.newCloudUtilities(logger)
|
||||
let fileStorage = FILE_STORAGE.newFileStorage(logger)
|
||||
|
||||
let statusDependencies;
|
||||
let dataDependenciesModule;
|
||||
let dataFiles = new Map;
|
||||
let indicatorOutputModule;
|
||||
|
||||
let bootstrappingTheProcess = false
|
||||
|
||||
let processConfig;
|
||||
let beginingOfMarket
|
||||
|
||||
return thisObject;
|
||||
|
||||
function initialize(pProcessConfig, pStatusDependencies, pDataDependencies, callBackFunction) {
|
||||
logger.fileName = MODULE_NAME;
|
||||
logger.initialize()
|
||||
|
||||
statusDependencies = pStatusDependencies;
|
||||
dataDependenciesModule = pDataDependencies;
|
||||
processConfig = pProcessConfig;
|
||||
|
||||
let INDICATOR_OUTPUT_MODULE = require("./IndicatorOutput")
|
||||
|
||||
indicatorOutputModule = INDICATOR_OUTPUT_MODULE.newIndicatorOutput(bot, logger, UTILITIES, FILE_STORAGE)
|
||||
indicatorOutputModule.initialize(callBackFunction)
|
||||
}
|
||||
|
||||
function finalize() {
|
||||
dataFiles = undefined
|
||||
statusDependencies = undefined
|
||||
dataDependenciesModule = undefined
|
||||
indicatorOutputModule = undefined
|
||||
fileStorage = undefined
|
||||
processConfig = undefined
|
||||
thisObject = undefined
|
||||
}
|
||||
|
||||
function start(callBackFunction) {
|
||||
try {
|
||||
let market = bot.market;
|
||||
|
||||
/* Context Variables */
|
||||
let contextVariables = {
|
||||
lastFile: undefined, // Datetime of the last file files sucessfully produced by this process.
|
||||
dateBeginOfMarket: undefined, // Datetime of the first trade file in the whole market history.
|
||||
dateEndOfMarket: undefined // Datetime of the last file available to be used as an input of this process.
|
||||
};
|
||||
|
||||
let previousDay; // Holds the date of the previous day relative to the processing date.
|
||||
let interExecutionMemoryArray;
|
||||
|
||||
getContextVariables()
|
||||
|
||||
function getContextVariables() {
|
||||
try {
|
||||
let thisReport;
|
||||
let statusReport;
|
||||
/*
|
||||
We look first for the bot who knows the begining of the market in order to get when the market starts.
|
||||
*/
|
||||
statusReport = statusDependencies.reportsByMainUtility.get("Market Starting Point")
|
||||
|
||||
if (statusReport === undefined) { // This means the status report does not exist, that could happen for instance at the begining of a month.
|
||||
logger.write(MODULE_NAME, "[WARN] start -> getContextVariables -> Market Starting Point -> Status Report does not exist or Market Starting Point not defined. Retrying Later. ")
|
||||
callBackFunction(global.DEFAULT_RETRY_RESPONSE)
|
||||
return
|
||||
}
|
||||
|
||||
if (statusReport.status === "Status Report is corrupt.") {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> getContextVariables -> Can not continue because dependecy Status Report is corrupt. ")
|
||||
callBackFunction(global.DEFAULT_RETRY_RESPONSE)
|
||||
return
|
||||
}
|
||||
|
||||
thisReport = statusReport.file;
|
||||
|
||||
if (thisReport.beginingOfMarket === undefined) {
|
||||
logger.write(MODULE_NAME, "[WARN] start -> getContextVariables -> Undefined Last File. -> thisReport.beginingOfMarket === undefined ")
|
||||
logger.write(MODULE_NAME, "[HINT] start -> getContextVariables -> It is too early too run this process since the trade history of the market is not there yet.")
|
||||
|
||||
let customOK = {
|
||||
result: global.CUSTOM_OK_RESPONSE.result,
|
||||
message: "Dependency does not exist."
|
||||
}
|
||||
logger.write(MODULE_NAME, "[WARN] start -> getContextVariables -> customOK = " + customOK.message)
|
||||
callBackFunction(customOK)
|
||||
return
|
||||
}
|
||||
|
||||
contextVariables.dateBeginOfMarket = new Date(thisReport.beginingOfMarket.year + "-" + thisReport.beginingOfMarket.month + "-" + thisReport.beginingOfMarket.days + " " + thisReport.beginingOfMarket.hours + ":" + thisReport.beginingOfMarket.minutes + GMT_SECONDS)
|
||||
/*
|
||||
Here we get the status report from the bot who knows which is the end of the market.
|
||||
*/
|
||||
statusReport = statusDependencies.reportsByMainUtility.get("Market Ending Point")
|
||||
|
||||
if (statusReport === undefined) { // This means the status report does not exist, that could happen for instance at the begining of a month.
|
||||
logger.write(MODULE_NAME, "[WARN] start -> getContextVariables -> Market Ending Point -> Status Report does not exist or Market Ending Point not defined. Retrying Later. ")
|
||||
callBackFunction(global.DEFAULT_RETRY_RESPONSE)
|
||||
return
|
||||
}
|
||||
|
||||
if (statusReport.status === "Status Report is corrupt.") {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> getContextVariables -> Can not continue because dependecy Status Report is corrupt. ")
|
||||
callBackFunction(global.DEFAULT_RETRY_RESPONSE)
|
||||
return
|
||||
}
|
||||
|
||||
thisReport = statusReport.file;
|
||||
|
||||
if (thisReport.lastFile === undefined) {
|
||||
logger.write(MODULE_NAME, "[WARN] start -> getContextVariables -> Undefined Last File. -> thisReport.lastFile === undefined")
|
||||
|
||||
let customOK = {
|
||||
result: global.CUSTOM_OK_RESPONSE.result,
|
||||
message: "Dependency not ready."
|
||||
}
|
||||
logger.write(MODULE_NAME, "[WARN] start -> getContextVariables -> customOK = " + customOK.message)
|
||||
callBackFunction(customOK)
|
||||
return
|
||||
}
|
||||
|
||||
contextVariables.dateEndOfMarket = new Date(thisReport.lastFile.valueOf())
|
||||
|
||||
/* Finally we get our own Status Report. */
|
||||
statusReport = statusDependencies.reportsByMainUtility.get("Self Reference")
|
||||
|
||||
if (statusReport === undefined) { // This means the status report does not exist, that could happen for instance at the begining of a month.
|
||||
logger.write(MODULE_NAME, "[WARN] start -> getContextVariables -> Self Reference -> Status Report does not exist or Self Reference not defined or badly configured. Check that at the Status Dependency you have chosen Self Reference at the config. Retrying Later. ")
|
||||
callBackFunction(global.DEFAULT_RETRY_RESPONSE)
|
||||
return
|
||||
}
|
||||
|
||||
if (statusReport.status === "Status Report is corrupt.") {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> getContextVariables -> Can not continue because self dependecy Status Report is corrupt. Aborting Process.")
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE)
|
||||
return
|
||||
}
|
||||
|
||||
thisReport = statusReport.file;
|
||||
|
||||
if (thisReport.lastFile !== undefined) {
|
||||
|
||||
beginingOfMarket = new Date(thisReport.beginingOfMarket)
|
||||
|
||||
if (beginingOfMarket.valueOf() !== contextVariables.dateBeginOfMarket.valueOf()) { // Reset Mechanism for Begining of the Market
|
||||
logger.write(MODULE_NAME, "[INFO] start -> getContextVariables -> Reset Mechanism for Begining of the Market Activated.")
|
||||
|
||||
beginingOfMarket = new Date(contextVariables.dateBeginOfMarket.valueOf())
|
||||
startFromBegining()
|
||||
return
|
||||
}
|
||||
|
||||
if (bot.hasTheBotJustStarted === true && processConfig.framework.startDate !== undefined && processConfig.framework.startDate.resumeExecution === false) {
|
||||
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> getContextVariables -> Starting from the begining because bot has just started and resume execution was true.") }
|
||||
startFromBegining()
|
||||
return
|
||||
}
|
||||
|
||||
contextVariables.lastFile = new Date(thisReport.lastFile)
|
||||
interExecutionMemoryArray = thisReport.interExecutionMemoryArray;
|
||||
|
||||
processTimeFrames()
|
||||
} else {
|
||||
|
||||
/*
|
||||
In the case when there is no status report, we assume like the last processed file is the one on the date of Begining of Market.
|
||||
*/
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> getContextVariables -> Starting from the begining of the market because own status report not found or lastFile was undefined.") }
|
||||
beginingOfMarket = new Date(contextVariables.dateBeginOfMarket.valueOf())
|
||||
startFromBegining()
|
||||
}
|
||||
|
||||
function startFromBegining() {
|
||||
contextVariables.lastFile = new Date(contextVariables.dateBeginOfMarket.getUTCFullYear() + "-" + (contextVariables.dateBeginOfMarket.getUTCMonth() + 1) + "-" + contextVariables.dateBeginOfMarket.getUTCDate() + " " + "00:00" + GMT_SECONDS)
|
||||
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> getContextVariables -> startFromBegining -> contextVariables.lastFile = " + contextVariables.lastFile) }
|
||||
/*
|
||||
The first time the process is running is the right time to create the data structure that is going to be shared across different executions.
|
||||
This data structure has one object per each timeFrame.
|
||||
*/
|
||||
interExecutionMemoryArray = []
|
||||
/*
|
||||
Also, we will remember that we are bootstrapping the process, this will allow us, to advanceTime if necesary.
|
||||
It can happen that a process depends on data from another process that does not produce a file during the first day, or even more days,
|
||||
in that situation we want the process not to just wait for that data that will never arrive, but to advance time until it finds valid data.
|
||||
*/
|
||||
bootstrappingTheProcess = true
|
||||
|
||||
for (let i = 0; i < global.dailyFilePeriods.length; i++) {
|
||||
let emptyObject = {};
|
||||
interExecutionMemoryArray.push(emptyObject)
|
||||
}
|
||||
|
||||
processTimeFrames()
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> getContextVariables -> err = " + err.stack)
|
||||
if (err.message === "Cannot read property 'file' of undefined") {
|
||||
logger.write(MODULE_NAME, "[HINT] start -> getContextVariables -> Check the bot configuration to see if all of its statusDependencies declarations are correct. ")
|
||||
logger.write(MODULE_NAME, "[HINT] start -> getContextVariables -> Dependencies loaded -> keys = " + JSON.stringify(statusDependencies.keys))
|
||||
}
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE)
|
||||
}
|
||||
}
|
||||
|
||||
function processTimeFrames() {
|
||||
let n;
|
||||
let botNeverRan = true;
|
||||
|
||||
bot.multiPeriodDailyProcessDatetime = new Date(contextVariables.lastFile.valueOf() - ONE_DAY_IN_MILISECONDS) // Go back one day to start well when we advance time at the begining of the loop.
|
||||
let fromDate = new Date(bot.multiPeriodDailyProcessDatetime.valueOf())
|
||||
let lastDate = new Date()
|
||||
|
||||
advanceTime()
|
||||
|
||||
function advanceTime() {
|
||||
bot.multiPeriodDailyProcessDatetime = new Date(bot.multiPeriodDailyProcessDatetime.valueOf() + ONE_DAY_IN_MILISECONDS)
|
||||
previousDay = new Date(bot.multiPeriodDailyProcessDatetime.valueOf() - ONE_DAY_IN_MILISECONDS)
|
||||
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> processTimeFrames -> advanceTime -> bot.multiPeriodDailyProcessDatetime = " + bot.multiPeriodDailyProcessDatetime) }
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> processTimeFrames -> advanceTime -> previousDay = " + previousDay) }
|
||||
|
||||
/* Validation that we are not going past the head of the market. */
|
||||
if (bot.multiPeriodDailyProcessDatetime.valueOf() > contextVariables.dateEndOfMarket.valueOf()) {
|
||||
|
||||
const logText = "Head of the market found @ " + previousDay.getUTCFullYear() + "/" + (previousDay.getUTCMonth() + 1) + "/" + previousDay.getUTCDate() + ".";
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> processTimeFrames -> advanceTime -> " + logText) }
|
||||
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE)
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
/* Telling the world we are alive and doing well */
|
||||
let currentDateString = bot.multiPeriodDailyProcessDatetime.getUTCFullYear() + '-' + utilities.pad(bot.multiPeriodDailyProcessDatetime.getUTCMonth() + 1, 2) + '-' + utilities.pad(bot.multiPeriodDailyProcessDatetime.getUTCDate(), 2)
|
||||
let currentDate = new Date(bot.multiPeriodDailyProcessDatetime)
|
||||
let percentage = global.getPercentage(fromDate, currentDate, lastDate)
|
||||
bot.processHeartBeat(currentDateString, percentage)
|
||||
|
||||
if (global.areEqualDates(currentDate, new Date()) === false) {
|
||||
logger.newInternalLoop(bot.codeName, bot.process, currentDate, percentage)
|
||||
}
|
||||
checkStopTaskGracefully()
|
||||
}
|
||||
function checkStopTaskGracefully() {
|
||||
/* Validation that we dont need to stop. */
|
||||
if (global.STOP_TASK_GRACEFULLY === true) {
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE)
|
||||
return
|
||||
}
|
||||
timeFramesLoop()
|
||||
}
|
||||
|
||||
function timeFramesLoop() {
|
||||
/*
|
||||
We will iterate through all posible timeFrames.
|
||||
*/
|
||||
n = 0 // loop Variable representing each possible period as defined at the timeFrames array.
|
||||
timeFramesLoopBody()
|
||||
}
|
||||
|
||||
function timeFramesLoopBody() {
|
||||
|
||||
const timeFrame = global.dailyFilePeriods[n][0]
|
||||
const timeFrameLabel = global.dailyFilePeriods[n][1]
|
||||
|
||||
/* Check Time Frames Filter */
|
||||
if (bot.dailyTimeFrames !== undefined) {
|
||||
if (bot.dailyTimeFrames.includes(timeFrameLabel) === false) {
|
||||
/* We are not going to process this Time Frame */
|
||||
timeFramesControlLoop()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (processConfig.framework.validPeriods !== undefined) {
|
||||
let validPeriod = false;
|
||||
for (let i = 0; i < processConfig.framework.validPeriods.length; i++) {
|
||||
let period = processConfig.framework.validPeriods[i]
|
||||
if (period === timeFrameLabel) { validPeriod = true }
|
||||
}
|
||||
if (validPeriod === false) {
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> processTimeFrames -> timeFramesLoopBody -> Discarding period for not being listed as a valid period. -> timeFrameLabel = " + timeFrameLabel) }
|
||||
timeFramesControlLoop()
|
||||
return
|
||||
}
|
||||
}
|
||||
let dependencyIndex = 0
|
||||
dataFiles = new Map()
|
||||
|
||||
dependencyLoopBody()
|
||||
|
||||
function dependencyLoopBody() {
|
||||
let dependency = dataDependenciesModule.nodeArray[dependencyIndex]
|
||||
if (dependency === undefined) {
|
||||
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> processTimeFrames -> timeFramesLoopBody -> dependencyLoopBody -> You need to add at least one Data Dependency to the process Multi Period Daily. Aborting process.")
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE)
|
||||
return
|
||||
}
|
||||
|
||||
let datasetModule = dataDependenciesModule.dataSetsModulesArray[dependencyIndex]
|
||||
let previousFile
|
||||
let currentFile
|
||||
|
||||
if (bot.multiPeriodDailyProcessDatetime.valueOf() > contextVariables.dateBeginOfMarket.valueOf()) {
|
||||
getPreviousFile()
|
||||
} else {
|
||||
previousFile = []
|
||||
getCurrentFile()
|
||||
}
|
||||
|
||||
function getPreviousFile() {
|
||||
let dateForPath = previousDay.getUTCFullYear() + '/' + utilities.pad(previousDay.getUTCMonth() + 1, 2) + '/' + utilities.pad(previousDay.getUTCDate(), 2)
|
||||
let filePath
|
||||
if (dependency.referenceParent.config.codeName === "Multi-Period-Daily") {
|
||||
filePath = dependency.referenceParent.parentNode.config.codeName + '/' + dependency.referenceParent.config.codeName + "/" + timeFrameLabel + "/" + dateForPath;
|
||||
} else {
|
||||
filePath = dependency.referenceParent.parentNode.config.codeName + '/' + dependency.referenceParent.config.codeName + "/" + dateForPath;
|
||||
}
|
||||
let fileName = "Data.json"
|
||||
|
||||
datasetModule.getTextFile(filePath, fileName, onFileReceived)
|
||||
|
||||
function onFileReceived(err, text) {
|
||||
if ((err.message === "File does not exist." && botNeverRan === true) || err.code === 'The specified key does not exist.') {
|
||||
/*
|
||||
Sometimes one of the dependencies of an indicator for some reasons are not calculated from the begining of the market.
|
||||
When that happens we can not find those files. What we do in this situation is to move the time fordward until we can find
|
||||
all the dependencies and the first run of the bot is successful.
|
||||
|
||||
After that, we will not accept more missing files on any of the dependencies, and if any is missing we will abort the processing.
|
||||
*/
|
||||
logger.write(MODULE_NAME, "[WARN] start -> processTimeFrames -> timeFramesLoopBody -> dependencyLoopBody -> getPreviousFile -> onFileReceived -> Skipping day because file " + filePath + "/" + fileName + " was not found.")
|
||||
|
||||
advanceTime()
|
||||
return
|
||||
}
|
||||
|
||||
if ((err.result === "Fail Because" && err.message === "File does not exist.") || err.code === 'The specified key does not exist.') {
|
||||
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> processTimeFrames -> timeFramesLoopBody -> dependencyLoopBody -> getPreviousFile -> onFileReceived -> err = " + err.stack)
|
||||
callBackFunction(global.DEFAULT_RETRY_RESPONSE)
|
||||
return
|
||||
}
|
||||
|
||||
if (err.result !== global.DEFAULT_OK_RESPONSE.result) {
|
||||
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> processTimeFrames -> timeFramesLoopBody -> dependencyLoopBody -> getPreviousFile -> onFileReceived -> err = " + err.stack)
|
||||
callBackFunction(err)
|
||||
return
|
||||
}
|
||||
|
||||
previousFile = JSON.parse(text)
|
||||
getCurrentFile()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function getCurrentFile() {
|
||||
let dateForPath = bot.multiPeriodDailyProcessDatetime.getUTCFullYear() + '/' + utilities.pad(bot.multiPeriodDailyProcessDatetime.getUTCMonth() + 1, 2) + '/' + utilities.pad(bot.multiPeriodDailyProcessDatetime.getUTCDate(), 2)
|
||||
let filePath
|
||||
if (dependency.referenceParent.config.codeName === "Multi-Period-Daily") {
|
||||
filePath = dependency.referenceParent.parentNode.config.codeName + '/' + dependency.referenceParent.config.codeName + "/" + timeFrameLabel + "/" + dateForPath;
|
||||
} else {
|
||||
filePath = dependency.referenceParent.parentNode.config.codeName + '/' + dependency.referenceParent.config.codeName + "/" + dateForPath;
|
||||
}
|
||||
let fileName = "Data.json";
|
||||
|
||||
datasetModule.getTextFile(filePath, fileName, onFileReceived)
|
||||
|
||||
function onFileReceived(err, text) {
|
||||
if ((err.result === "Fail Because" && err.message === "File does not exist.") || err.code === 'The specified key does not exist.') {
|
||||
if (bootstrappingTheProcess === true) {
|
||||
/*
|
||||
In the special situation where we are running this process for the first time (no status report found),
|
||||
we will consider the possibility that a depanant process does not produce a file during the first day of the market,
|
||||
or even more than one day. That is why we are going to advance time here.
|
||||
*/
|
||||
advanceTime()
|
||||
return
|
||||
}
|
||||
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> processTimeFrames -> timeFramesLoopBody -> dependencyLoopBody -> getCurrentFile -> onFileReceived -> err = " + err.code)
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> processTimeFrames -> timeFramesLoopBody -> dependencyLoopBody -> getCurrentFile -> onFileReceived -> filePath = " + filePath)
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> processTimeFrames -> timeFramesLoopBody -> dependencyLoopBody -> getCurrentFile -> onFileReceived -> fileName = " + fileName)
|
||||
callBackFunction(global.DEFAULT_RETRY_RESPONSE)
|
||||
return
|
||||
}
|
||||
|
||||
if (err.result !== global.DEFAULT_OK_RESPONSE.result) {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> processTimeFrames -> timeFramesLoopBody -> dependencyLoopBody -> getCurrentFile -> Not OK -> onFileReceived -> err = " + err.code)
|
||||
callBackFunction(err)
|
||||
return
|
||||
}
|
||||
|
||||
currentFile = JSON.parse(text)
|
||||
let dataFile = previousFile.concat(currentFile)
|
||||
dataFiles.set(dependency.id, dataFile)
|
||||
dependencyControlLoop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function dependencyControlLoop() {
|
||||
dependencyIndex++;
|
||||
if (dependencyIndex < dataDependenciesModule.nodeArray.length) {
|
||||
dependencyLoopBody()
|
||||
} else {
|
||||
generateOutput()
|
||||
}
|
||||
|
||||
function generateOutput() {
|
||||
|
||||
indicatorOutputModule.start(
|
||||
dataFiles,
|
||||
timeFrame,
|
||||
timeFrameLabel,
|
||||
bot.multiPeriodDailyProcessDatetime,
|
||||
interExecutionMemoryArray[n],
|
||||
onOutputGenerated)
|
||||
|
||||
function onOutputGenerated(err) {
|
||||
if (err.result !== global.DEFAULT_OK_RESPONSE.result) {
|
||||
callBackFunction(err)
|
||||
return
|
||||
}
|
||||
botNeverRan = false;
|
||||
timeFramesControlLoop()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function timeFramesControlLoop() {
|
||||
n++
|
||||
if (n < global.dailyFilePeriods.length) {
|
||||
timeFramesLoopBody()
|
||||
} else {
|
||||
n = 0;
|
||||
writeDataRanges(onWritten)
|
||||
|
||||
function onWritten(err) {
|
||||
if (err.result !== global.DEFAULT_OK_RESPONSE.result) {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> processTimeFrames -> controlLoop -> onWritten -> err = " + err.stack)
|
||||
callBackFunction(err)
|
||||
return
|
||||
}
|
||||
writeStatusReport(bot.multiPeriodDailyProcessDatetime, advanceTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function writeDataRanges(callBack) {
|
||||
let outputDatasets = global.NODE_BRANCH_TO_ARRAY (bot.processNode.referenceParent.processOutput, 'Output Dataset')
|
||||
let outputDatasetIndex = -1;
|
||||
controlLoop()
|
||||
|
||||
function productLoopBody() {
|
||||
let productCodeName = outputDatasets[outputDatasetIndex].referenceParent.parentNode.config.codeName;
|
||||
writeDataRange(contextVariables.dateBeginOfMarket, bot.multiPeriodDailyProcessDatetime, productCodeName, controlLoop)
|
||||
}
|
||||
|
||||
function controlLoop() {
|
||||
outputDatasetIndex++
|
||||
if (outputDatasetIndex < outputDatasets.length) {
|
||||
productLoopBody()
|
||||
} else {
|
||||
callBack(global.DEFAULT_OK_RESPONSE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function writeDataRange(pBegin, pEnd, productCodeName, callBack) {
|
||||
let dataRange = {
|
||||
begin: pBegin.valueOf(),
|
||||
end: pEnd.valueOf() + ONE_DAY_IN_MILISECONDS
|
||||
};
|
||||
let fileContent = JSON.stringify(dataRange)
|
||||
let fileName = '/Data.Range.json';
|
||||
let filePath = bot.filePathRoot + "/Output/" + productCodeName + "/" + bot.process + fileName;
|
||||
|
||||
fileStorage.createTextFile(filePath, fileContent + '\n', onFileCreated)
|
||||
|
||||
function onFileCreated(err) {
|
||||
if (err.result !== global.DEFAULT_OK_RESPONSE.result) {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> writeDataRange -> onFileCreated -> err = " + err.stack)
|
||||
callBack(err)
|
||||
return
|
||||
}
|
||||
let key = bot.dataMine + "-" + bot.codeName + "-" + productCodeName + "-" + bot.exchange + "-" + bot.market.baseAsset + '/' + bot.market.quotedAsset
|
||||
let event = {
|
||||
dateRange: dataRange
|
||||
}
|
||||
|
||||
global.EVENT_SERVER_CLIENT.raiseEvent(key, 'Data Range Updated', event)
|
||||
writeTimeFramesFile(productCodeName, callBack)
|
||||
}
|
||||
}
|
||||
|
||||
function writeTimeFramesFile(productCodeName, callBack) {
|
||||
|
||||
let timeFramesArray = []
|
||||
for (let n = 0; n < global.dailyFilePeriods.length; n++) {
|
||||
let timeFrameLabel = global.dailyFilePeriods[n][1]
|
||||
|
||||
/* Check Time Frames Filter */
|
||||
if (bot.dailyTimeFrames !== undefined) {
|
||||
if (bot.dailyTimeFrames.includes(timeFrameLabel) === true) {
|
||||
timeFramesArray.push(timeFrameLabel)
|
||||
}
|
||||
} else {
|
||||
timeFramesArray.push(timeFrameLabel)
|
||||
}
|
||||
}
|
||||
|
||||
let fileContent = JSON.stringify(timeFramesArray)
|
||||
let fileName = '/Time.Frames.json';
|
||||
let filePath = bot.filePathRoot + "/Output/" + productCodeName + "/" + bot.process + fileName;
|
||||
|
||||
fileStorage.createTextFile(filePath, fileContent + '\n', onFileCreated)
|
||||
|
||||
function onFileCreated(err) {
|
||||
if (err.result !== global.DEFAULT_OK_RESPONSE.result) {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> writeTimeFramesFiles -> onFileCreated -> err = " + err.stack)
|
||||
callBack(err)
|
||||
return
|
||||
}
|
||||
callBack(global.DEFAULT_OK_RESPONSE)
|
||||
}
|
||||
}
|
||||
|
||||
function writeStatusReport(lastFileDate, callBack) {
|
||||
let reportKey = bot.dataMine + "-" + bot.codeName + "-" + "Multi-Period-Daily"
|
||||
let thisReport = statusDependencies.statusReports.get(reportKey)
|
||||
|
||||
thisReport.file.lastExecution = bot.currentDaytime;
|
||||
thisReport.file.lastFile = lastFileDate;
|
||||
thisReport.file.interExecutionMemoryArray = interExecutionMemoryArray;
|
||||
thisReport.file.beginingOfMarket = beginingOfMarket.toUTCString()
|
||||
if (bot.dailyTimeFrames !== undefined) {
|
||||
thisReport.file.timeFrames = bot.dailyTimeFrames
|
||||
}
|
||||
thisReport.save(callBack)
|
||||
|
||||
bot.hasTheBotJustStarted = false;
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> err = " + err.stack)
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE)
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,221 @@
|
||||
exports.newMultiPeriodMarket = function newMultiPeriodMarket(bot, logger, UTILITIES, FILE_STORAGE) {
|
||||
const MODULE_NAME = "Multi Period Market";
|
||||
thisObject = {
|
||||
initialize: initialize,
|
||||
finalize: finalize,
|
||||
start: start
|
||||
};
|
||||
|
||||
let utilities = UTILITIES.newCloudUtilities(logger)
|
||||
let fileStorage = FILE_STORAGE.newFileStorage(logger)
|
||||
|
||||
let statusDependencies;
|
||||
let dataDependenciesModule;
|
||||
let dataFiles = new Map()
|
||||
let indicatorOutputModule;
|
||||
let processConfig;
|
||||
|
||||
return thisObject;
|
||||
|
||||
function initialize(pProcessConfig, pStatusDependencies, pDataDependencies, callBackFunction) {
|
||||
logger.fileName = MODULE_NAME;
|
||||
logger.initialize()
|
||||
|
||||
statusDependencies = pStatusDependencies;
|
||||
dataDependenciesModule = pDataDependencies;
|
||||
processConfig = pProcessConfig;
|
||||
|
||||
let INDICATOR_OUTPUT_MODULE = require("./IndicatorOutput")
|
||||
|
||||
indicatorOutputModule = INDICATOR_OUTPUT_MODULE.newIndicatorOutput(bot, logger, UTILITIES, FILE_STORAGE)
|
||||
indicatorOutputModule.initialize(callBackFunction)
|
||||
}
|
||||
|
||||
function finalize() {
|
||||
fileStorage = undefined
|
||||
dataFiles = undefined
|
||||
statusDependencies = undefined
|
||||
dataDependenciesModule = undefined
|
||||
indicatorOutputModule = undefined
|
||||
processConfig = undefined
|
||||
thisObject = undefined
|
||||
}
|
||||
|
||||
function start(callBackFunction) {
|
||||
try {
|
||||
processTimeFrames()
|
||||
|
||||
function processTimeFrames() {
|
||||
let n;
|
||||
timeFramesLoop()
|
||||
|
||||
function timeFramesLoop() {
|
||||
/* We will iterate through all posible timeFrames.*/
|
||||
n = 0 // loop Variable representing each possible period as defined at the timeFrames array.
|
||||
timeFramesLoopBody()
|
||||
}
|
||||
|
||||
function timeFramesLoopBody() {
|
||||
const timeFrame = global.marketFilesPeriods[n][0]
|
||||
const timeFrameLabel = global.marketFilesPeriods[n][1]
|
||||
|
||||
/* Check Time Frames Filter */
|
||||
if (bot.marketTimeFrames !== undefined) {
|
||||
if (bot.marketTimeFrames.includes(timeFrameLabel) === false) {
|
||||
/* We are not going to process this Time Frame */
|
||||
timeFramesControlLoop()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let dependencyIndex = 0;
|
||||
dataFiles = new Map;
|
||||
|
||||
dependencyLoopBody()
|
||||
|
||||
function dependencyLoopBody() {
|
||||
let dependency = dataDependenciesModule.nodeArray[dependencyIndex]
|
||||
let datasetModule = dataDependenciesModule.dataSetsModulesArray[dependencyIndex]
|
||||
|
||||
getFile()
|
||||
|
||||
function getFile() {
|
||||
let fileName = "Data.json";
|
||||
let filePath = dependency.referenceParent.parentNode.config.codeName + '/' + dependency.referenceParent.config.codeName + "/" + timeFrameLabel;
|
||||
datasetModule.getTextFile(filePath, fileName, onFileReceived)
|
||||
|
||||
function onFileReceived(err, text) {
|
||||
if (err.result !== global.DEFAULT_OK_RESPONSE.result) {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> processTimeFrames -> timeFramesLoopBody -> dependencyLoopBody -> getFile -> onFileReceived -> err = " + err.stack)
|
||||
callBackFunction(err)
|
||||
return;
|
||||
}
|
||||
|
||||
let dataFile = JSON.parse(text)
|
||||
dataFiles.set(dependency.id, dataFile)
|
||||
|
||||
dependencyControlLoop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function dependencyControlLoop() {
|
||||
dependencyIndex++;
|
||||
if (dependencyIndex < dataDependenciesModule.nodeArray.length) {
|
||||
dependencyLoopBody()
|
||||
} else {
|
||||
generateOutput()
|
||||
}
|
||||
|
||||
function generateOutput() {
|
||||
indicatorOutputModule.start(
|
||||
dataFiles,
|
||||
timeFrame,
|
||||
timeFrameLabel,
|
||||
undefined,
|
||||
undefined,
|
||||
onOutputGenerated)
|
||||
|
||||
function onOutputGenerated(err) {
|
||||
if (err.result !== global.DEFAULT_OK_RESPONSE.result) {
|
||||
callBackFunction(err)
|
||||
return;
|
||||
}
|
||||
timeFramesControlLoop()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function timeFramesControlLoop() {
|
||||
n++;
|
||||
if (n < global.marketFilesPeriods.length) {
|
||||
timeFramesLoopBody()
|
||||
} else {
|
||||
writeTimeFramesFiles(onTimeFrameFilesWritten)
|
||||
function onTimeFrameFilesWritten() {
|
||||
writeStatusReport(callBackFunction)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function writeTimeFramesFiles(callBack) {
|
||||
let outputDatasets = global.NODE_BRANCH_TO_ARRAY (bot.processNode.referenceParent.processOutput, 'Output Dataset')
|
||||
let outputDatasetIndex = -1;
|
||||
controlLoop()
|
||||
|
||||
function productLoopBody() {
|
||||
let productCodeName = outputDatasets[outputDatasetIndex].referenceParent.parentNode.config.codeName;
|
||||
writeTimeFramesFile(productCodeName, controlLoop)
|
||||
}
|
||||
|
||||
function controlLoop() {
|
||||
outputDatasetIndex++
|
||||
if (outputDatasetIndex < outputDatasets.length) {
|
||||
productLoopBody()
|
||||
} else {
|
||||
callBack()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function writeTimeFramesFile(productCodeName, callBack) {
|
||||
|
||||
let timeFramesArray = []
|
||||
for (let n = 0; n < global.marketFilesPeriods.length; n++) {
|
||||
let timeFrameLabel = global.marketFilesPeriods[n][1]
|
||||
|
||||
/* Check Time Frames Filter */
|
||||
if (bot.marketTimeFrames !== undefined) {
|
||||
if (bot.marketTimeFrames.includes(timeFrameLabel) === true) {
|
||||
timeFramesArray.push(timeFrameLabel)
|
||||
}
|
||||
} else {
|
||||
timeFramesArray.push(timeFrameLabel)
|
||||
}
|
||||
}
|
||||
|
||||
let fileContent = JSON.stringify(timeFramesArray)
|
||||
let fileName = '/Time.Frames.json';
|
||||
let filePath = bot.filePathRoot + "/Output/" + productCodeName + "/" + bot.process + fileName;
|
||||
|
||||
fileStorage.createTextFile(filePath, fileContent + '\n', onFileCreated)
|
||||
|
||||
function onFileCreated(err) {
|
||||
if (err.result !== global.DEFAULT_OK_RESPONSE.result) {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> writeTimeFramesFile -> onFileCreated -> err = " + err.stack)
|
||||
callBack(err)
|
||||
return
|
||||
}
|
||||
callBack()
|
||||
}
|
||||
}
|
||||
|
||||
function writeStatusReport(callBack) {
|
||||
let reportKey = bot.dataMine + "-" + bot.codeName + "-" + "Multi-Period-Market"
|
||||
let thisReport = statusDependencies.statusReports.get(reportKey)
|
||||
|
||||
thisReport.file.lastExecution = bot.processDatetime;
|
||||
if (bot.marketTimeFrames !== undefined) {
|
||||
thisReport.file.timeFrames = bot.marketTimeFrames
|
||||
}
|
||||
thisReport.save(callBack)
|
||||
|
||||
if (global.areEqualDates(bot.processDatetime, new Date()) === false) {
|
||||
logger.newInternalLoop(bot.codeName, bot.process, bot.processDatetime)
|
||||
}
|
||||
|
||||
/* Telling the world we are alive and doing well */
|
||||
let currentDateString = bot.processDatetime.getUTCFullYear() + '-' + utilities.pad(bot.processDatetime.getUTCMonth() + 1, 2) + '-' + utilities.pad(bot.processDatetime.getUTCDate(), 2)
|
||||
let currentDate = new Date(bot.processDatetime)
|
||||
let lastDate = new Date()
|
||||
bot.processHeartBeat(currentDateString, global.getPercentage(currentDate, currentDate, lastDate))
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> err = " + err.stack)
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
exports.newProcessExecutionEvents = function newProcessExecutionEvents(BOT, logger) {
|
||||
/*
|
||||
Here we manage the process execution events. This allow us to emit events
|
||||
when the process starts and finishes. Also to wait for a dependent event
|
||||
of another process.
|
||||
*/
|
||||
const MODULE_NAME = "Process Execution Events";
|
||||
logger.fileName = MODULE_NAME
|
||||
|
||||
let bot = BOT;
|
||||
|
||||
let thisObject = {
|
||||
networkNode: undefined,
|
||||
initialize: initialize,
|
||||
finalize: finalize,
|
||||
start: start,
|
||||
finish: finish
|
||||
};
|
||||
|
||||
let processThisDependsOn; // This is the process this process needs to wait for it to finishes in order to start.
|
||||
let currentProcessKey
|
||||
|
||||
let EVENT_SERVER_CLIENT = require('./EventServerClient.js');
|
||||
let eventServerClient
|
||||
|
||||
return thisObject;
|
||||
|
||||
function initialize(processConfig, callBackFunction) {
|
||||
try {
|
||||
let name = 'Not Depends on any Process'
|
||||
logger.fileName = MODULE_NAME + "." + name;
|
||||
|
||||
if (bot.processNode.referenceParent === undefined) {
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
logger.write(MODULE_NAME, "[ERROR] initialize -> Initialization Failed because the Process Instance needs to have a Reference Parent.");
|
||||
return
|
||||
}
|
||||
|
||||
let market = bot.market.baseAsset + '/' + bot.market.quotedAsset
|
||||
currentProcessKey = bot.processNode.referenceParent.name + "-" + bot.processNode.referenceParent.type + "-" + bot.processNode.referenceParent.id + "-" + bot.exchange + "-" + market
|
||||
|
||||
/*
|
||||
A process can depend for its own execution on another process to finish, and
|
||||
this can be achieved in two different ways. The first one is by referencing
|
||||
the Execution Started Event node from a Process Definition node to a Execution
|
||||
Ended Event node of another Process Definition. The second way is when there is
|
||||
a Execution Started Event node at a Process Instance. This allows the user to
|
||||
override the definition created by the Data Mine for an specific task.
|
||||
*/
|
||||
|
||||
/*
|
||||
Here we check if we can find this relationship at the Data Mine level.
|
||||
*/
|
||||
if (bot.processNode.referenceParent.executionStartedEvent !== undefined) {
|
||||
if (bot.processNode.referenceParent.executionStartedEvent.referenceParent !== undefined) {
|
||||
if (bot.processNode.referenceParent.executionStartedEvent.referenceParent.parentNode !== undefined) {
|
||||
processThisDependsOn = bot.processNode.referenceParent.executionStartedEvent.referenceParent.parentNode
|
||||
|
||||
name = processThisDependsOn.name
|
||||
if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { logger.write(MODULE_NAME, "[INFO] initialize -> " + currentProcessKey + " based on the Data Mine depends on " + name); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Here we check if at the task level if there is an overriding configuration.
|
||||
*/
|
||||
if (bot.processNode.executionStartedEvent !== undefined) {
|
||||
if (bot.processNode.executionStartedEvent.referenceParent !== undefined) {
|
||||
if (bot.processNode.executionStartedEvent.referenceParent.parentNode !== undefined) {
|
||||
processThisDependsOn = bot.processNode.executionStartedEvent.referenceParent.parentNode
|
||||
|
||||
name = processThisDependsOn.name
|
||||
if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { logger.write(MODULE_NAME, "[INFO] initialize -> " + currentProcessKey + " based on the User's Task depends on " + name); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (processThisDependsOn !== undefined) {
|
||||
|
||||
/* We need to remember that this process waits for another process in order to start. */
|
||||
processConfig.waitsForExecutionFinishedEvent = true
|
||||
|
||||
/* We need to find at which network node is running the process that we need to hear from when it finished. */
|
||||
let network = global.TASK_NETWORK
|
||||
|
||||
/*
|
||||
Notice that the first task that matches the dependency is going to be considered.
|
||||
This means that if the same task, with the same process, pointing to the same
|
||||
process definition in the same data mine, with the same exchange and market
|
||||
configured, is at two different network nodes at the same time, the current task
|
||||
is going to depend on the first one found, not necesarily the one running at the
|
||||
same network node. If you need to control witch task specifically to depend on
|
||||
and need to exactly the same task to exist, you should split the network in two
|
||||
different networks, each one with unique tasks so as to avoid confussion.
|
||||
*/
|
||||
for (let i = 0; i < network.networkNodes.length; i++) {
|
||||
let networkNode = network.networkNodes[i]
|
||||
if (networkNode.dataMining !== undefined) {
|
||||
for (let j = 0; j < networkNode.dataMining.exchangeDataTasks.length; j++) {
|
||||
let exchangeTasks = networkNode.dataMining.exchangeDataTasks[j]
|
||||
for (let p = 0; p < exchangeTasks.marketDataTasks.length; p++) {
|
||||
let marketTasks = exchangeTasks.marketDataTasks[p]
|
||||
if (marketTasks.referenceParent === undefined) { continue }
|
||||
if (marketTasks.referenceParent.id !== global.MARKET_NODE.id) { continue }
|
||||
for (let q = 0; q < marketTasks.dataMineTasks.length; q++) {
|
||||
let mineTasks = marketTasks.dataMineTasks[q]
|
||||
for (let k = 0; k < mineTasks.taskManagers.length; k++) {
|
||||
let taskManager = mineTasks.taskManagers[k]
|
||||
for (let m = 0; m < taskManager.tasks.length; m++) {
|
||||
let task = taskManager.tasks[m]
|
||||
if (task.bot !== undefined) {
|
||||
for (let n = 0; n < task.bot.processes.length; n++) {
|
||||
let process = task.bot.processes[n]
|
||||
if (process.referenceParent !== undefined) {
|
||||
let processDefinition = process.referenceParent
|
||||
if (processThisDependsOn.id === processDefinition.id) {
|
||||
/*
|
||||
We found where the task that runs the process definition
|
||||
we are waiting for is located on the network.
|
||||
*/
|
||||
thisObject.networkNode = networkNode
|
||||
if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) {
|
||||
logger.write(MODULE_NAME,
|
||||
"[INFO] initialize -> Connecting to Websockets Server " + networkNode.name +
|
||||
" -> host = " + networkNode.config.host +
|
||||
' -> port = ' + networkNode.config.webSocketsPort + '.');
|
||||
}
|
||||
|
||||
eventServerClient = EVENT_SERVER_CLIENT.newEventsServerClient(networkNode.config.host, networkNode.config.webSocketsPort)
|
||||
eventServerClient.initialize(onConnected)
|
||||
|
||||
function onConnected() {
|
||||
if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) {
|
||||
logger.write(MODULE_NAME, "[INFO] initialize -> Connected to Websockets Server " + networkNode.name +
|
||||
" -> host = " + networkNode.config.host +
|
||||
' -> port = ' + networkNode.config.webSocketsPort + '.');
|
||||
}
|
||||
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE);
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
logger.write(MODULE_NAME, "[ERROR] initialize -> Initialization Failed because we could not find where the task that runs the Process Definition we are waiting for is located within the network. Check the logs for more info.");
|
||||
return
|
||||
}
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE);
|
||||
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] initialize -> err = " + err.stack);
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
}
|
||||
}
|
||||
|
||||
function finalize() {
|
||||
if (eventServerClient !== undefined) {
|
||||
eventServerClient.finalize()
|
||||
eventServerClient = undefined
|
||||
}
|
||||
|
||||
processThisDependsOn = undefined
|
||||
currentProcessKey = undefined
|
||||
thisObject.networkNode = undefined
|
||||
bot = undefined
|
||||
thisObject = undefined
|
||||
}
|
||||
|
||||
function start(callBackFunction) {
|
||||
|
||||
try {
|
||||
|
||||
if (processThisDependsOn !== undefined) {
|
||||
|
||||
if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { logger.write(MODULE_NAME, "[INFO] start -> waitForDependantProcess -> Entering Code Block."); }
|
||||
|
||||
/* This forces this process to wait until the process that this one depends on, emits its event signaling that the process execution has finished. */
|
||||
|
||||
let extraCallerId = '-' + Math.trunc(Math.random() * 10000) + '-'
|
||||
|
||||
let market = bot.market.baseAsset + '/' + bot.market.quotedAsset
|
||||
let key = processThisDependsOn.name + "-" + processThisDependsOn.type + "-" + processThisDependsOn.id + "-" + bot.exchange + "-" + market
|
||||
let callerId = bot.dataMine + "-" + bot.codeName + "-" + bot.process + extraCallerId
|
||||
|
||||
let subscriptionId
|
||||
|
||||
if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { logger.write(MODULE_NAME, "[INFO] start -> waitForDependantProcess -> started listening to Process Execution Finished. "); }
|
||||
if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { logger.write(MODULE_NAME, "[INFO] start -> waitForDependantProcess -> key = " + key); }
|
||||
if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { logger.write(MODULE_NAME, "[INFO] start -> waitForDependantProcess -> callerId = " + callerId); }
|
||||
|
||||
eventServerClient.listenToEvent(key, 'Process Execution Finished', undefined, callerId, responseCallBack, eventsCallBack)
|
||||
|
||||
bot.processHeartBeat(undefined, undefined, "Waiting for " + thisObject.networkNode.name + "->" + processThisDependsOn.parentNode.parentNode.name + "->" + processThisDependsOn.parentNode.config.codeName + "->" + processThisDependsOn.config.codeName)
|
||||
|
||||
function responseCallBack(message) {
|
||||
if (message.result !== global.DEFAULT_OK_RESPONSE.result) {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> responseCallBack -> message = " + JSON.stringify(message))
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
} else {
|
||||
subscriptionId = message.eventSubscriptionId
|
||||
if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { logger.write(MODULE_NAME, "[INFO] start -> waitForDependantProcess -> subscriptionId = " + subscriptionId); }
|
||||
}
|
||||
}
|
||||
|
||||
function eventsCallBack(message) {
|
||||
if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { logger.write(MODULE_NAME, "[INFO] start -> eventsCallBack -> Entering function."); }
|
||||
if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { logger.write(MODULE_NAME, "[INFO] start -> eventsCallBack -> message = " + JSON.stringify(message)); }
|
||||
|
||||
/* We continue the normal flow after we learn the dependent process has finished its execution. */
|
||||
if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { logger.write(MODULE_NAME, "[INFO] start -> eventsCallBack -> stopListening to Process Execution Finished. "); }
|
||||
if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { logger.write(MODULE_NAME, "[INFO] start -> eventsCallBack -> subscriptionId = " + subscriptionId); }
|
||||
eventServerClient.stopListening(key, 'Process Execution Finished', subscriptionId)
|
||||
|
||||
if (message.event.err.result === global.DEFAULT_OK_RESPONSE.result) {
|
||||
/* We emit our own event that the process started. */
|
||||
let event = {
|
||||
err: global.DEFAULT_OK_RESPONSE
|
||||
}
|
||||
global.EVENT_SERVER_CLIENT.createEventHandler(currentProcessKey, 'Process Execution Started')
|
||||
global.EVENT_SERVER_CLIENT.raiseEvent(currentProcessKey, 'Process Execution Started', event)
|
||||
|
||||
bot.processHeartBeat(undefined, undefined, "Running...")
|
||||
|
||||
if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { logger.write(MODULE_NAME, "[INFO] start -> eventsCallBack -> " + currentProcessKey + " Process Execution Started because " + key + " Finished."); }
|
||||
}
|
||||
|
||||
callBackFunction(message.event.err);
|
||||
}
|
||||
} else {
|
||||
/* In this case, the Process does not depends on a process which needs to wait for. */
|
||||
/* We emit our own event that the process started. */
|
||||
let event = {
|
||||
err: global.DEFAULT_OK_RESPONSE
|
||||
}
|
||||
global.EVENT_SERVER_CLIENT.createEventHandler(currentProcessKey, 'Process Execution Started')
|
||||
global.EVENT_SERVER_CLIENT.raiseEvent(currentProcessKey, 'Process Execution Started', event)
|
||||
|
||||
if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { logger.write(MODULE_NAME, "[INFO] start -> " + currentProcessKey + " Process Execution Started "); }
|
||||
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> err = " + err.stack);
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
}
|
||||
}
|
||||
|
||||
function finish(callBackFunction) {
|
||||
|
||||
try {
|
||||
|
||||
if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { logger.write(MODULE_NAME, "[INFO] finish -> Entering function."); }
|
||||
|
||||
let event = {
|
||||
err: global.DEFAULT_OK_RESPONSE
|
||||
}
|
||||
|
||||
global.EVENT_SERVER_CLIENT.createEventHandler(currentProcessKey, 'Process Execution Finished')
|
||||
global.EVENT_SERVER_CLIENT.raiseEvent(currentProcessKey, 'Process Execution Finished', event)
|
||||
|
||||
if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { logger.write(MODULE_NAME, "[INFO] finish -> " + currentProcessKey + " Process Execution Finished "); }
|
||||
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE);
|
||||
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] finish -> err = " + err.stack);
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
@@ -0,0 +1,184 @@
|
||||
exports.newProcessOutput = function newProcessOutput(BOT, logger) {
|
||||
|
||||
/*
|
||||
Here we emit the events that signals that a Dataset was updated.
|
||||
*/
|
||||
|
||||
const MODULE_NAME = "Process Output";
|
||||
|
||||
let bot = BOT;
|
||||
|
||||
let thisObject = {
|
||||
raiseEvents: raiseEvents,
|
||||
asyncRaiseEvents: asyncRaiseEvents
|
||||
};
|
||||
|
||||
return thisObject;
|
||||
|
||||
function raiseEvents(lastFile, timeFrames, callBackFunction) {
|
||||
try {
|
||||
/* We will reaise the events for the datasets impacted by the process that just finished. */
|
||||
|
||||
if (bot.processNode.referenceParent.processOutput !== undefined) {
|
||||
let outputDatasets = global.NODE_BRANCH_TO_ARRAY(bot.processNode.referenceParent.processOutput, 'Output Dataset')
|
||||
|
||||
for (let j = 0; j < outputDatasets.length; j++) {
|
||||
let outputDataset = outputDatasets[j]
|
||||
|
||||
let botNode = global.FIND_NODE_IN_NODE_MESH(outputDataset, 'Indicator Bot')
|
||||
if (botNode === undefined) {
|
||||
botNode = global.FIND_NODE_IN_NODE_MESH(outputDataset, 'Trading Bot')
|
||||
}
|
||||
if (botNode === undefined) {
|
||||
botNode = global.FIND_NODE_IN_NODE_MESH(outputDataset, 'Sensor Bot')
|
||||
}
|
||||
if (botNode === undefined) {
|
||||
botNode = global.FIND_NODE_IN_NODE_MESH(outputDataset, 'Learning Bot')
|
||||
}
|
||||
|
||||
/* Some validations to verify that everything is in place. */
|
||||
if (outputDataset.referenceParent !== undefined) {
|
||||
if (outputDataset.referenceParent.config.codeName === undefined) {
|
||||
logger.write(MODULE_NAME, "[ERROR] raiseEvents -> codeName at Dataset Definition not defined. Dataset Definition = " + JSON.stringify(outputDataset.referenceParent));
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
return
|
||||
}
|
||||
if (outputDataset.referenceParent.parentNode !== undefined) {
|
||||
if (outputDataset.referenceParent.parentNode.config.codeName === undefined) {
|
||||
logger.write(MODULE_NAME, "[ERROR] raiseEvents -> codeName at Product not defined. Product = " + JSON.stringify(outputDataset.referenceParent.parentNode));
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
return
|
||||
}
|
||||
if (botNode !== undefined) {
|
||||
if (botNode.config.codeName === undefined) {
|
||||
logger.write(MODULE_NAME, "[ERROR] raiseEvents -> codeName at Bot not defined. Bot = " + JSON.stringify(botNode));
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
return
|
||||
}
|
||||
if (botNode.parentNode !== undefined) {
|
||||
if (botNode.parentNode.config.codeName === undefined) {
|
||||
logger.write(MODULE_NAME, "[ERROR] raiseEvents -> codeName at Data Mine not defined. Data Mine = " + JSON.stringify(botNode.parentNode));
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
return
|
||||
}
|
||||
} else {
|
||||
logger.write(MODULE_NAME, "[ERROR] raiseEvents -> Bot not attached to a Data Mine or Trading Mine. Bot = " + JSON.stringify(botNode));
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
return
|
||||
}
|
||||
} else {
|
||||
logger.write(MODULE_NAME, "[ERROR] raiseEvents -> Product not attached to a Bot. Product = " + JSON.stringify(outputDataset.referenceParent.parentNode));
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
return
|
||||
}
|
||||
} else {
|
||||
logger.write(MODULE_NAME, "[ERROR] raiseEvents -> Dataset Definition not attached to a Product. Dataset Definition = " + JSON.stringify(outputDataset.referenceParent));
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
return
|
||||
}
|
||||
} else {
|
||||
logger.write(MODULE_NAME, "[ERROR] raiseEvents -> Output Dataset with no reference to a Dataset Definition. Output Dataset = " + JSON.stringify(outputDataset));
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
return
|
||||
}
|
||||
|
||||
/* All good, lets emit the event. */
|
||||
|
||||
let dataMine = botNode.parentNode.config.codeName
|
||||
let botName = botNode.config.codeName
|
||||
let product = outputDataset.referenceParent.parentNode.config.codeName
|
||||
let dataset = outputDataset.referenceParent.config.codeName
|
||||
|
||||
let key = dataMine + "-" + botName + "-" + product + "-" + dataset + "-" + bot.exchange + "-" + bot.market.baseAsset + '/' + bot.market.quotedAsset
|
||||
let event = {
|
||||
lastFile: lastFile,
|
||||
timeFrames: timeFrames
|
||||
}
|
||||
global.EVENT_SERVER_CLIENT.createEventHandler(key, 'Dataset Updated')
|
||||
global.EVENT_SERVER_CLIENT.raiseEvent(key, 'Dataset Updated', event)
|
||||
}
|
||||
}
|
||||
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE);
|
||||
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] raiseEvents -> err = " + err.stack);
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
}
|
||||
}
|
||||
|
||||
function asyncRaiseEvents(lastFile, timeFrames) {
|
||||
|
||||
if (bot.processNode.referenceParent.processOutput !== undefined) {
|
||||
let outputDatasets = global.NODE_BRANCH_TO_ARRAY(bot.processNode.referenceParent.processOutput, 'Output Dataset')
|
||||
|
||||
for (let j = 0; j < outputDatasets.length; j++) {
|
||||
let outputDataset = outputDatasets[j]
|
||||
|
||||
let botNode = global.FIND_NODE_IN_NODE_MESH(outputDataset, 'Indicator Bot')
|
||||
if (botNode === undefined) {
|
||||
botNode = global.FIND_NODE_IN_NODE_MESH(outputDataset, 'Trading Bot')
|
||||
}
|
||||
if (botNode === undefined) {
|
||||
botNode = global.FIND_NODE_IN_NODE_MESH(outputDataset, 'Sensor Bot')
|
||||
}
|
||||
if (botNode === undefined) {
|
||||
botNode = global.FIND_NODE_IN_NODE_MESH(outputDataset, 'Learning Bot')
|
||||
}
|
||||
|
||||
/* Some validations to verify that everything is in place. */
|
||||
if (outputDataset.referenceParent !== undefined) {
|
||||
if (outputDataset.referenceParent.config.codeName === undefined) {
|
||||
logger.write(MODULE_NAME, "[ERROR] raiseEvents -> codeName at Dataset Definition not defined. Dataset Definition = " + JSON.stringify(outputDataset.referenceParent));
|
||||
throw (global.DEFAULT_FAIL_RESPONSE);
|
||||
}
|
||||
if (outputDataset.referenceParent.parentNode !== undefined) {
|
||||
if (outputDataset.referenceParent.parentNode.config.codeName === undefined) {
|
||||
logger.write(MODULE_NAME, "[ERROR] raiseEvents -> codeName at Product not defined. Product = " + JSON.stringify(outputDataset.referenceParent.parentNode));
|
||||
throw (global.DEFAULT_FAIL_RESPONSE);
|
||||
}
|
||||
if (botNode !== undefined) {
|
||||
if (botNode.config.codeName === undefined) {
|
||||
logger.write(MODULE_NAME, "[ERROR] raiseEvents -> codeName at Bot not defined. Bot = " + JSON.stringify(botNode));
|
||||
throw (global.DEFAULT_FAIL_RESPONSE);
|
||||
}
|
||||
if (botNode.parentNode !== undefined) {
|
||||
if (botNode.parentNode.config.codeName === undefined) {
|
||||
logger.write(MODULE_NAME, "[ERROR] raiseEvents -> codeName at Mine not defined. Mine = " + JSON.stringify(botNode.parentNode));
|
||||
throw (global.DEFAULT_FAIL_RESPONSE);
|
||||
}
|
||||
} else {
|
||||
logger.write(MODULE_NAME, "[ERROR] raiseEvents -> Bot not attached to a Data Mine | Trading Mine | Learning Mine. Bot = " + JSON.stringify(botNode));
|
||||
throw (global.DEFAULT_FAIL_RESPONSE);
|
||||
}
|
||||
} else {
|
||||
logger.write(MODULE_NAME, "[ERROR] raiseEvents -> Product not attached to a Bot. Product = " + JSON.stringify(outputDataset.referenceParent.parentNode));
|
||||
throw (global.DEFAULT_FAIL_RESPONSE);
|
||||
}
|
||||
} else {
|
||||
logger.write(MODULE_NAME, "[ERROR] raiseEvents -> Dataset Definition not attached to a Product. Dataset Definition = " + JSON.stringify(outputDataset.referenceParent));
|
||||
throw (global.DEFAULT_FAIL_RESPONSE);
|
||||
}
|
||||
} else {
|
||||
logger.write(MODULE_NAME, "[ERROR] raiseEvents -> Output Dataset with no reference to a Dataset Definition. Output Dataset = " + JSON.stringify(outputDataset));
|
||||
throw (global.DEFAULT_FAIL_RESPONSE);
|
||||
}
|
||||
|
||||
/* All good, lets emit the event. */
|
||||
|
||||
let dataMine = botNode.parentNode.config.codeName
|
||||
let botName = botNode.config.codeName
|
||||
let product = outputDataset.referenceParent.parentNode.config.codeName
|
||||
let dataset = outputDataset.referenceParent.config.codeName
|
||||
|
||||
let key = dataMine + "-" + botName + "-" + product + "-" + dataset + "-" + bot.exchange + "-" + bot.market.baseAsset + '/' + bot.market.quotedAsset
|
||||
let event = {
|
||||
lastFile: lastFile,
|
||||
timeFrames: timeFrames
|
||||
}
|
||||
global.EVENT_SERVER_CLIENT.createEventHandler(key, 'Dataset Updated')
|
||||
global.EVENT_SERVER_CLIENT.raiseEvent(key, 'Dataset Updated', event)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,685 @@
|
||||
exports.newLearningBot = function newLearningBot(bot, parentLogger) {
|
||||
|
||||
const MODULE_NAME = "Learning Bot";
|
||||
const FULL_LOG = true;
|
||||
|
||||
const TRADING_PROCESS_MODULE = require(global.ROOT_DIR + '/LowFrequencyTrading/MachineLearningProcess.js');
|
||||
const FILE_STORAGE = require('./FileStorage.js');
|
||||
const SESSION = require(global.ROOT_DIR + 'TradingSession');
|
||||
|
||||
let fileStorage = FILE_STORAGE.newFileStorage(parentLogger);
|
||||
let session = SESSION.newLearningSession(bot, parentLogger)
|
||||
|
||||
const DEBUG_MODULE = require(global.ROOT_DIR + 'DebugLog');
|
||||
let logger; // We need this here in order for the loopHealth function to work and be able to rescue the loop when it gets in trouble.
|
||||
|
||||
let nextLoopTimeoutHandle;
|
||||
|
||||
let thisObject = {
|
||||
initialize: initialize,
|
||||
run: run
|
||||
};
|
||||
|
||||
let processConfig;
|
||||
|
||||
return thisObject;
|
||||
|
||||
function initialize(pProcessConfig, callBackFunction) {
|
||||
/* This function is exactly the same in the 3 modules representing the 2 different bot types loops. */
|
||||
try {
|
||||
processConfig = pProcessConfig;
|
||||
if (bot.definedByUI === true) {
|
||||
/* The code of the bot is defined at the UI. No need to load a file with the code. */
|
||||
session.initialize(processConfig, callBackFunction)
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] initialize -> err = " + err.stack);
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
}
|
||||
}
|
||||
|
||||
function run(callBackFunction) {
|
||||
try {
|
||||
/* Some initial values*/
|
||||
let fixedTimeLoopIntervalHandle;
|
||||
bot.STOP_SESSION = true;
|
||||
if (FULL_LOG === true) { parentLogger.write(MODULE_NAME, '[IMPORTANT] run -> Stopping the Session now. ') }
|
||||
|
||||
/* Heartbeats sent to the UI */
|
||||
bot.processHeartBeat = processHeartBeat
|
||||
|
||||
loop();
|
||||
|
||||
function loop() {
|
||||
try {
|
||||
processHeartBeat(undefined, undefined, "Running...")
|
||||
function pad(str, max) {
|
||||
str = str.toString();
|
||||
return str.length < max ? pad(" " + str, max) : str;
|
||||
}
|
||||
|
||||
/* For each loop we want to create a new log file. */
|
||||
if (logger !== undefined) {
|
||||
logger.finalize()
|
||||
}
|
||||
logger = DEBUG_MODULE.newDebugLog();
|
||||
global.LOGGER_MAP.set(MODULE_NAME, logger)
|
||||
logger.bot = bot;
|
||||
logger.initialize();
|
||||
|
||||
bot.loopCounter++;
|
||||
bot.loopStartTime = new Date().valueOf();
|
||||
|
||||
let nextWaitTime;
|
||||
|
||||
/* We tell the UI that we are running. */
|
||||
processHeartBeat(undefined, undefined, "Running...")
|
||||
|
||||
/* We define here all the modules that the rest of the infraestructure, including the bots themselves can consume. */
|
||||
const UTILITIES = require(global.ROOT_DIR + 'CloudUtilities');
|
||||
const STATUS_REPORT = require(global.ROOT_DIR + 'StatusReport');
|
||||
const STATUS_DEPENDENCIES = require(global.ROOT_DIR + 'StatusDependencies');
|
||||
const DATA_DEPENDENCIES = require(global.ROOT_DIR + 'DataDependencies');
|
||||
const DATA_SET = require(global.ROOT_DIR + 'DataSet');
|
||||
const PROCESS_EXECUTION_EVENTS = require(global.ROOT_DIR + 'ProcessExecutionEvents');
|
||||
const PROCESS_OUTPUT = require(global.ROOT_DIR + 'ProcessOutput');
|
||||
|
||||
/* We define the datetime for the process that we are running now. This will be the official processing time for both the infraestructure and the bot. */
|
||||
bot.processDatetime = new Date(); // This will be considered the process date and time, so as to have it consistenly all over the execution.
|
||||
|
||||
/* High level log entry */
|
||||
console.log(new Date().toISOString() + " " + pad(bot.exchange, 20) + " " + pad(bot.market.baseAsset + '/' + bot.market.quotedAsset, 10) + " " + pad(bot.codeName, 30) + " " + pad(bot.process, 30)
|
||||
+ " Main Loop # " + pad(Number(bot.loopCounter), 8) + " " + bot.processNode.session.type + " " + bot.processNode.session.name)
|
||||
|
||||
/* Checking if we need to need to emit any event */
|
||||
if (bot.SESSION_STATUS === 'Idle' && bot.STOP_SESSION === false) {
|
||||
bot.SESSION_STATUS = 'Running'
|
||||
}
|
||||
|
||||
if (bot.SESSION_STATUS === 'Running' && bot.STOP_SESSION === true) {
|
||||
bot.SESSION_STATUS = 'Stopped'
|
||||
}
|
||||
|
||||
global.EMIT_SESSION_STATUS (bot.SESSION_STATUS, bot.sessionKey)
|
||||
|
||||
/* Checking if we should process this loop or not.*/
|
||||
if (bot.STOP_SESSION === true) {
|
||||
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] run -> loop -> Waiting for " + bot.processNode.session.type + " " + bot.processNode.session.name + " to be run."); }
|
||||
|
||||
console.log(new Date().toISOString() + " " + pad(bot.codeName, 20) + " " + pad(bot.process, 30)
|
||||
+ " Waiting for " + bot.processNode.session.type + " " + bot.processNode.session.name + " to be run. ");
|
||||
|
||||
nextWaitTime = 'Waiting for Session';
|
||||
loopControl(nextWaitTime);
|
||||
return
|
||||
}
|
||||
|
||||
/* We will prepare first the infraestructure needed for the bot to run. There are 3 modules we need to sucessfullly initialize first. */
|
||||
|
||||
let processExecutionEvents
|
||||
let userBot;
|
||||
let processFramework;
|
||||
let statusDependencies;
|
||||
let dataDependencies;
|
||||
|
||||
initializeProcessExecutionEvents();
|
||||
|
||||
function initializeProcessExecutionEvents() {
|
||||
try {
|
||||
processExecutionEvents = PROCESS_EXECUTION_EVENTS.newProcessExecutionEvents(bot, logger)
|
||||
processExecutionEvents.initialize(processConfig, onInizialized);
|
||||
|
||||
function onInizialized(err) {
|
||||
try {
|
||||
switch (err.result) {
|
||||
case global.DEFAULT_OK_RESPONSE.result: {
|
||||
logger.write(MODULE_NAME, "[INFO] run -> loop -> initializeProcessExecutionEvents -> onInizialized -> Execution finished well.");
|
||||
startProcessExecutionEvents()
|
||||
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, "[WARN] run -> loop -> initializeProcessExecutionEvents -> onInizialized -> Retry Later. Requesting Execution Retry.");
|
||||
nextWaitTime = 'Retry';
|
||||
loopControl(nextWaitTime);
|
||||
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] run -> loop -> initializeProcessExecutionEvents -> onInizialized -> Operation Failed. Aborting the process.");
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
default: {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeProcessExecutionEvents -> onInizialized -> Unhandled err.result received. -> err.result = " + err.result);
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeProcessExecutionEvents -> onInizialized -> Unhandled err.result received. -> err = " + err.message);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeProcessExecutionEvents -> onInizialized -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeProcessExecutionEvents -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function startProcessExecutionEvents() {
|
||||
try {
|
||||
processExecutionEvents.start(onStarted);
|
||||
|
||||
function onStarted(err) {
|
||||
try {
|
||||
switch (err.result) {
|
||||
case global.DEFAULT_OK_RESPONSE.result: {
|
||||
logger.write(MODULE_NAME, "[INFO] run -> loop -> startProcessExecutionEvents -> onStarted -> Execution finished well.");
|
||||
|
||||
if (global.STOP_TASK_GRACEFULLY === true) {
|
||||
loopControl()
|
||||
return
|
||||
}
|
||||
|
||||
initializeStatusDependencies();
|
||||
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, "[WARN] run -> loop -> startProcessExecutionEvents -> onStarted -> Retry Later. Requesting Execution Retry.");
|
||||
nextWaitTime = 'Retry';
|
||||
loopControl(nextWaitTime);
|
||||
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] run -> loop -> startProcessExecutionEvents -> onStarted -> Operation Failed. Aborting the process.");
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
default: {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> startProcessExecutionEvents -> onStarted -> Unhandled err.result received. -> err.result = " + err.result);
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> startProcessExecutionEvents -> onStarted -> Unhandled err.result received. -> err = " + err.message);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> startProcessExecutionEvents -> onStarted -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> startProcessExecutionEvents -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function initializeStatusDependencies() {
|
||||
try {
|
||||
statusDependencies = STATUS_DEPENDENCIES.newStatusDependencies(bot, logger, STATUS_REPORT, UTILITIES, PROCESS_OUTPUT);
|
||||
statusDependencies.initialize(onInizialized);
|
||||
|
||||
function onInizialized(err) {
|
||||
try {
|
||||
switch (err.result) {
|
||||
case global.DEFAULT_OK_RESPONSE.result: {
|
||||
logger.write(MODULE_NAME, "[INFO] run -> loop -> initializeStatusDependencies -> onInizialized -> Execution finished well.");
|
||||
initializeDataDependencies();
|
||||
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, "[WARN] run -> loop -> initializeStatusDependencies -> onInizialized -> Retry Later. Requesting Execution Retry.");
|
||||
nextWaitTime = 'Retry';
|
||||
loopControl(nextWaitTime);
|
||||
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] run -> loop -> initializeStatusDependencies -> onInizialized -> Operation Failed. Aborting the process.");
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
default: {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeStatusDependencies -> onInizialized -> Unhandled err.result received. -> err.result = " + err.result);
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeStatusDependencies -> onInizialized -> Unhandled err.result received. -> err = " + err.message);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeStatusDependencies -> onInizialized -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeStatusDependencies -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function initializeDataDependencies() {
|
||||
try {
|
||||
dataDependencies = DATA_DEPENDENCIES.newDataDependencies(bot, logger, DATA_SET);
|
||||
dataDependencies.initialize(onInizialized);
|
||||
|
||||
function onInizialized(err) {
|
||||
try {
|
||||
switch (err.result) {
|
||||
case global.DEFAULT_OK_RESPONSE.result: {
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] run -> loop -> initializeDataDependencies -> onInizialized -> Execution finished well."); }
|
||||
switch (processConfig.framework.name) {
|
||||
case 'Low-Frequency-Trading-Process': {
|
||||
processFramework = TRADING_PROCESS_MODULE.newMachineLearningProcess(bot, logger, UTILITIES);
|
||||
intitializeProcessFramework();
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeDataDependencies -> onInizialized -> Process Framework not Supported.");
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeDataDependencies -> onInizialized -> Process Framework Name = " + processConfig.framework.name);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
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, "[WARN] run -> loop -> initializeDataDependencies -> onInizialized -> Retry Later. Requesting Execution Retry.");
|
||||
nextWaitTime = 'Retry';
|
||||
loopControl(nextWaitTime);
|
||||
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] run -> loop -> initializeDataDependencies -> onInizialized -> Operation Failed. Aborting the process.");
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
default: {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeDataDependencies -> onInizialized -> Unhandled err.result received. -> err.result = " + err.result);
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeDataDependencies -> onInizialized -> Unhandled err.result received. -> err = " + err.message);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeDataDependencies -> onInizialized -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeDataDependencies -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function intitializeProcessFramework() {
|
||||
try {
|
||||
processFramework.initialize(processConfig, statusDependencies, dataDependencies, onInizialized);
|
||||
|
||||
function onInizialized(err) {
|
||||
try {
|
||||
switch (err.result) {
|
||||
case global.DEFAULT_OK_RESPONSE.result: {
|
||||
logger.write(MODULE_NAME, "[INFO] run -> loop -> intitializeProcessFramework -> onInizialized -> Execution finished well.");
|
||||
startProcessFramework();
|
||||
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, "[WARN] run -> loop -> intitializeProcessFramework -> onInizialized -> Retry Later. Requesting Execution Retry.");
|
||||
nextWaitTime = 'Retry';
|
||||
loopControl(nextWaitTime);
|
||||
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] run -> loop -> intitializeProcessFramework -> onInizialized -> Operation Failed. Aborting the process.");
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
case global.CUSTOM_OK_RESPONSE.result: {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> intitializeProcessFramework -> onInizialized > Unhandled custom response received. -> err = " + err.message);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
default: {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> intitializeProcessFramework -> onInizialized -> Unhandled err.result received. -> err.result = " + err.result);
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> intitializeProcessFramework -> onInizialized -> Unhandled err.result received. -> err = " + err.message);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> intitializeProcessFramework -> onInizialized -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> intitializeProcessFramework -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function startProcessFramework() {
|
||||
try {
|
||||
processFramework.start(onFinished);
|
||||
|
||||
function onFinished(err) {
|
||||
try {
|
||||
processFramework.finalize()
|
||||
processFramework = undefined
|
||||
dataDependencies.finalize()
|
||||
dataDependencies = undefined
|
||||
statusDependencies.finalize()
|
||||
statusDependencies = undefined
|
||||
|
||||
switch (err.result) {
|
||||
case global.DEFAULT_OK_RESPONSE.result: {
|
||||
logger.write(MODULE_NAME, "[INFO] run -> loop -> startProcessFramework -> onFinished -> Execution finished well.");
|
||||
nextWaitTime = 'Normal';
|
||||
finishProcessExecutionEvents()
|
||||
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, "[WARN] run -> loop -> startProcessFramework -> onFinished -> Retry Later. Requesting Execution Retry.");
|
||||
nextWaitTime = 'Retry';
|
||||
loopControl(nextWaitTime);
|
||||
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] run -> loop -> startProcessFramework -> onFinished -> Operation Failed. Aborting the process.");
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
case global.CUSTOM_OK_RESPONSE.result: {
|
||||
|
||||
switch (err.message) {
|
||||
case "Dependency does not exist.": {
|
||||
logger.write(MODULE_NAME, "[WARN] run -> loop -> startProcessFramework -> onFinished -> Dependency does not exist. This Loop will go to sleep.");
|
||||
nextWaitTime = 'Sleep';
|
||||
loopControl(nextWaitTime);
|
||||
return;
|
||||
}
|
||||
case "Dependency not ready.": {
|
||||
logger.write(MODULE_NAME, "[WARN] run -> loop -> startProcessFramework -> onFinished -> Dependency not ready. This Loop will go to sleep.");
|
||||
nextWaitTime = 'Sleep';
|
||||
loopControl(nextWaitTime);
|
||||
return;
|
||||
}
|
||||
default: {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> startProcessFramework -> onFinished -> Unhandled custom response received. -> err = " + err.message);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
default: {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> startProcessFramework -> onFinished -> Unhandled err.result received. -> err.result = " + err.result);
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> startProcessFramework -> onFinished -> Unhandled err.result received. -> err = " + err.message);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> startProcessFramework -> onFinished -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> startProcessFramework -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function finishProcessExecutionEvents() {
|
||||
try {
|
||||
processExecutionEvents.finish(onFinished);
|
||||
|
||||
function onFinished(err) {
|
||||
try {
|
||||
processExecutionEvents.finalize()
|
||||
processExecutionEvents = undefined
|
||||
|
||||
switch (err.result) {
|
||||
case global.DEFAULT_OK_RESPONSE.result: {
|
||||
logger.write(MODULE_NAME, "[INFO] run -> loop -> finishProcessExecutionEvents -> onFinished -> Execution finished well.");
|
||||
nextWaitTime = 'Normal';
|
||||
loopControl(nextWaitTime);
|
||||
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, "[WARN] run -> loop -> finishProcessExecutionEvents -> onFinished -> Retry Later. Requesting Execution Retry.");
|
||||
nextWaitTime = 'Retry';
|
||||
loopControl(nextWaitTime);
|
||||
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] run -> loop -> finishProcessExecutionEvents -> onFinished -> Operation Failed. Aborting the process.");
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
default: {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> finishProcessExecutionEvents -> onFinished -> Unhandled err.result received. -> err.result = " + err.result);
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> finishProcessExecutionEvents -> onFinished -> Unhandled err.result received. -> err = " + err.message);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> finishProcessExecutionEvents -> onFinished -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> finishProcessExecutionEvents -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function loopControl(nextWaitTime) {
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] run -> loop -> loopControl -> nextWaitTime = " + nextWaitTime); }
|
||||
|
||||
/* We show we reached the end of the loop. */
|
||||
processHeartBeat(undefined, undefined, "Running...")
|
||||
|
||||
/* Here we check if we must stop the loop gracefully. */
|
||||
shallWeStop(onStop, onContinue);
|
||||
|
||||
function onStop() {
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] run -> loop -> loopControl -> onStop -> Stopping the Loop Gracefully. See you next time!"); }
|
||||
|
||||
if (global.WRITE_LOGS_TO_FILES === 'true') {
|
||||
logger.persist();
|
||||
}
|
||||
|
||||
global.EVENT_SERVER_CLIENT.raiseEvent(bot.sessionKey, 'Stopped')
|
||||
processStopped()
|
||||
return;
|
||||
}
|
||||
|
||||
function onContinue() {
|
||||
/* Indicator bots are going to be executed after a configured period of time after the last execution ended. This is to avoid overlapping executions. */
|
||||
switch (nextWaitTime) {
|
||||
case 'Waiting for Session': {
|
||||
let waitTime = processConfig.sessionRunWaitTime
|
||||
nextLoopTimeoutHandle = setTimeout(loop, waitTime);
|
||||
let waitingTime = waitTime / 1000 / 60
|
||||
let label = 'minute/s'
|
||||
if (waitingTime < 1) {
|
||||
waitingTime = waitTime / 1000
|
||||
label = 'seconds'
|
||||
}
|
||||
processHeartBeat(undefined, undefined, "Waiting " + waitingTime + " " + label + " for " + bot.processNode.session.type + " " + bot.processNode.session.name + " to be run. ")
|
||||
logger.persist();
|
||||
}
|
||||
break
|
||||
case 'Normal': {
|
||||
let waitTime
|
||||
if (processConfig.waitsForExecutionFinishedEvent === true) {
|
||||
waitTime = 0
|
||||
} else {
|
||||
switch (bot.LEARNING_SESSION.type) {
|
||||
case 'Live Trading Session': {
|
||||
waitTime = bot.LEARNING_SESSION.learningParameters.timeFrame.config.value
|
||||
break
|
||||
}
|
||||
case 'Fordward Tessting Session': {
|
||||
waitTime = bot.LEARNING_SESSION.learningParameters.timeFrame.config.value
|
||||
break
|
||||
}
|
||||
case 'Paper Trading Session': {
|
||||
waitTime = bot.LEARNING_SESSION.learningParameters.timeFrame.config.value
|
||||
break
|
||||
}
|
||||
case 'Backtesting Session': {
|
||||
waitTime = 0
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] run -> loop -> loopControl -> Restarting Loop in " + (waitTime / 1000 / 60) + " minute/s."); }
|
||||
nextLoopTimeoutHandle = setTimeout(loop, waitTime);
|
||||
let waitingTime = waitTime / 1000 / 60
|
||||
let label = 'minute/s'
|
||||
if (waitingTime < 1) {
|
||||
waitingTime = waitTime / 1000
|
||||
label = 'seconds'
|
||||
}
|
||||
processHeartBeat(undefined, undefined, "Waiting " + waitingTime + " " + label + " for next execution.")
|
||||
logger.persist();
|
||||
}
|
||||
break;
|
||||
case 'Retry': {
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] run -> loop -> loopControl -> Restarting Loop in " + (processConfig.retryWaitTime / 1000) + " seconds."); }
|
||||
nextLoopTimeoutHandle = setTimeout(loop, processConfig.retryWaitTime);
|
||||
processHeartBeat(undefined, undefined, "Trying to recover from some problem. Waiting " + processConfig.retryWaitTime / 1000 + " seconds for next execution.")
|
||||
logger.persist();
|
||||
}
|
||||
break;
|
||||
case 'Sleep': {
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] run -> loop -> loopControl -> Restarting Loop in " + (processConfig.sleepWaitTime / 60000) + " minutes."); }
|
||||
nextLoopTimeoutHandle = setTimeout(loop, processConfig.sleepWaitTime);
|
||||
processHeartBeat(undefined, undefined, "Waiting " + processConfig.sleepWaitTime / 60000 + " minutes for next execution.")
|
||||
logger.persist();
|
||||
}
|
||||
break;
|
||||
case 'Coma': {
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] run -> loop -> loopControl -> Restarting Loop in " + (processConfig.comaWaitTime / 3600000) + " hours."); }
|
||||
nextLoopTimeoutHandle = setTimeout(loop, processConfig.comaWaitTime);
|
||||
processHeartBeat(undefined, undefined, "Waiting " + processConfig.comaWaitTime / 3600000 + " hours for next execution.")
|
||||
logger.persist();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function shallWeStop(stopCallBack, continueCallBack) {
|
||||
try {
|
||||
/* IMPORTANT: This function is exactly the same on the 3 modules. */
|
||||
if (!global.STOP_TASK_GRACEFULLY) {
|
||||
continueCallBack();
|
||||
} else {
|
||||
stopCallBack();
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> shallWeStop -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] run -> loop -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function processHeartBeat(processingDate, percentage, status) {
|
||||
let event = {
|
||||
seconds: (new Date()).getSeconds(),
|
||||
processingDate: processingDate,
|
||||
percentage: percentage,
|
||||
status: status
|
||||
}
|
||||
global.EVENT_SERVER_CLIENT.raiseEvent(bot.processKey, 'Heartbeat', event)
|
||||
}
|
||||
|
||||
function processStopped() {
|
||||
if (global.unexpectedError !== undefined) {
|
||||
global.PROCESS_ERROR(bot.processKey, undefined, "An unexpected error caused the Process to stop.")
|
||||
} else {
|
||||
global.EVENT_SERVER_CLIENT.raiseEvent(bot.processKey, 'Stopped')
|
||||
}
|
||||
sessionStopped()
|
||||
logger.persist();
|
||||
clearInterval(fixedTimeLoopIntervalHandle);
|
||||
clearTimeout(nextLoopTimeoutHandle);
|
||||
if (global.unexpectedError !== undefined) {
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE)
|
||||
} else {
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE)
|
||||
}
|
||||
}
|
||||
|
||||
function sessionStopped() {
|
||||
if (bot.SESSION_STATUS === 'Running') {
|
||||
global.EVENT_SERVER_CLIENT.raiseEvent(bot.sessionKey, 'Stopped')
|
||||
bot.SESSION_STATUS = 'Stopped'
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] run -> err = " + err.stack);
|
||||
clearInterval(fixedTimeLoopIntervalHandle);
|
||||
clearTimeout(nextLoopTimeoutHandle);
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,786 @@
|
||||
exports.newLearningProcess = function newLearningProcess(bot, logger, UTILITIES) {
|
||||
/*
|
||||
This Module will load all the process data dependencies from files and send them downstream.
|
||||
After execution, will save the time range and status report of the process.
|
||||
*/
|
||||
const FULL_LOG = true;
|
||||
const MODULE_NAME = "Learning Process";
|
||||
const GMT_SECONDS = ':00.000 GMT+0000';
|
||||
|
||||
thisObject = {
|
||||
initialize: initialize,
|
||||
finalize: finalize,
|
||||
start: start
|
||||
};
|
||||
|
||||
let utilities = UTILITIES.newCloudUtilities(logger);
|
||||
|
||||
let statusDependencies;
|
||||
let dataDependenciesModule;
|
||||
let dataFiles = new Map();
|
||||
let multiPeriodDataFiles = new Map();
|
||||
|
||||
const FILE_STORAGE = require('../FileStorage.js');
|
||||
let fileStorage = FILE_STORAGE.newFileStorage(logger);
|
||||
|
||||
const MARKOV_DECISION_PROCESS_MODULE = require('./MarkovDecisionProcess.js')
|
||||
let markovDecisionProcessModule = MARKOV_DECISION_PROCESS_MODULE.newMarkovDecisionProcess(bot, logger)
|
||||
|
||||
let ALGORITHM_MODULE = require("./Algorithm")
|
||||
let algorithmModule = ALGORITHM_MODULE.newTradingOutput(bot, logger, markovDecisionProcessModule, UTILITIES, FILE_STORAGE)
|
||||
|
||||
let processConfig;
|
||||
|
||||
return thisObject;
|
||||
|
||||
function initialize(pProcessConfig, pStatusDependencies, pDataDependencies, callBackFunction) {
|
||||
try {
|
||||
logger.fileName = MODULE_NAME
|
||||
logger.initialize()
|
||||
|
||||
statusDependencies = pStatusDependencies
|
||||
dataDependenciesModule = pDataDependencies
|
||||
processConfig = pProcessConfig
|
||||
|
||||
algorithmModule.initialize()
|
||||
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE)
|
||||
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] initialize -> err = " + err.stack);
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
}
|
||||
}
|
||||
|
||||
function finalize() {
|
||||
|
||||
markovDecisionProcessModule.finalize()
|
||||
markovDecisionProcessModule = undefined
|
||||
|
||||
algorithmModule.finalize()
|
||||
algorithmModule = undefined
|
||||
|
||||
dataFiles = undefined
|
||||
multiPeriodDataFiles = undefined
|
||||
statusDependencies = undefined
|
||||
dataDependenciesModule = undefined
|
||||
fileStorage = undefined
|
||||
processConfig = undefined
|
||||
thisObject = undefined
|
||||
}
|
||||
|
||||
async function start(callBackFunction) {
|
||||
try {
|
||||
let currentTimeFrame
|
||||
let currentTimeFrameLabel
|
||||
|
||||
/* Context Variables */
|
||||
let contextVariables = {
|
||||
lastFile: undefined, // Datetime of the last file files sucessfully produced by this process.
|
||||
dateBeginOfMarket: undefined, // Datetime of the first trade file in the whole market history.
|
||||
dateEndOfMarket: undefined // Datetime of the last file available to be used as an input of this process.
|
||||
};
|
||||
|
||||
getContextVariables()
|
||||
|
||||
if (bot.FIRST_EXECUTION === true && bot.RESUME === false) {
|
||||
/*
|
||||
Here is where the Trading Engine and Trading Systems received are moved to the simulation state.
|
||||
*/
|
||||
bot.simulationState.tradingEngine = bot.TRADING_ENGINE
|
||||
bot.simulationState.tradingSystem = bot.TRADING_SYSTEM
|
||||
}
|
||||
|
||||
/* We set up the Trading Engine Module. */
|
||||
markovDecisionProcessModule.initialize()
|
||||
|
||||
/* Initializing the Trading Process Date */
|
||||
if (bot.FIRST_EXECUTION === true && bot.RESUME === false) {
|
||||
/*
|
||||
This funcion is going to be called many times by the Trading Bot Loop.
|
||||
Only during the first execution and when the User is not resuming the execution
|
||||
of a stopped session / task; we are going to initialize the Process Date Time.
|
||||
This variable tell us which day we are standing at, specially while working
|
||||
with Daily Files. From this Date is that we are going to load the Daily Files.
|
||||
*/
|
||||
bot.simulationState.tradingEngine.current.episode.processDate.value = global.REMOVE_TIME(bot.TRADING_SESSION.tradingParameters.timeRange.config.initialDatetime).valueOf()
|
||||
}
|
||||
|
||||
/*
|
||||
This is the Date that is going to be used across the execution of this Trading Process.
|
||||
We need this because it has a different life cycle than the processData stored at the
|
||||
Trading Engine data structure. This date has to remain the same during the whole execution
|
||||
of the Trading Process until the end, inclusind the writting of Data Ranges and Status Reports.
|
||||
The processDate of the Trading Engine data structure on the other hand can be changed during
|
||||
the simulation loop, once we discover that all candles from a certain date have benn processed.
|
||||
Here is the point where we sync one and the other.
|
||||
*/
|
||||
let LearningProcessDate = global.REMOVE_TIME(bot.simulationState.tradingEngine.current.episode.processDate.value)
|
||||
await processSingleFiles()
|
||||
|
||||
if (await processMarketFiles() === false) {
|
||||
bot.TRADING_SESSIONHeartBeat(undefined, undefined, 'Waiting for Data Mining to be run')
|
||||
callBackFunction(global.DEFAULT_RETRY_RESPONSE)
|
||||
return
|
||||
}
|
||||
/*
|
||||
This is the Data Structure used at the Simulation with all indicator data.
|
||||
We start creating it right here.
|
||||
*/
|
||||
let chart = {}
|
||||
/*
|
||||
Here we check if we need to get Daily Files or not. As an optimization, when
|
||||
we are running on a Time Frame of 1hs or above, we are not going to load
|
||||
dependencies on Daily Files. The way we recognize that is by checking if
|
||||
we alreaady set a value to currentTimeFrame. We are also not going to loop
|
||||
through days if we are processing market files.
|
||||
*/
|
||||
if (currentTimeFrame) {
|
||||
/* We are processing Market Files */
|
||||
/*
|
||||
With all the indicators data files loaded, we will build the chart object
|
||||
data structure that will be used in user-defied conditions and formulas.
|
||||
*/
|
||||
bot.TRADING_SESSIONHeartBeat(undefined, undefined, 'Waking up')
|
||||
buildCharts(chart)
|
||||
|
||||
if (checkThereAreCandles(chart) === true) {
|
||||
bot.TRADING_SESSIONHeartBeat(undefined, undefined, 'Running')
|
||||
await generateOutput(chart)
|
||||
bot.TRADING_SESSIONHeartBeat(undefined, undefined, 'Saving')
|
||||
await writeProcessFiles()
|
||||
bot.TRADING_SESSIONHeartBeat(undefined, undefined, 'Sleeping')
|
||||
} else {
|
||||
bot.TRADING_SESSIONHeartBeat(undefined, undefined, 'Waiting for Data Mining to be up to date. No candles found at.')
|
||||
callBackFunction(global.DEFAULT_RETRY_RESPONSE)
|
||||
return
|
||||
}
|
||||
|
||||
} else {
|
||||
/* We are processing Daily Files */
|
||||
do {
|
||||
global.EMIT_SESSION_STATUS(bot.TRADING_SESSION_STATUS, bot.TRADING_SESSIONKey)
|
||||
/*
|
||||
We update the Trading Process Date with the date calculated at the simulation.
|
||||
We will use this date to load indicator and output files. After that we will
|
||||
use it to save Output Files and later the Data Ranges. This is the point where
|
||||
the date calculated by the Simulation is applied at the Trading Process Level.
|
||||
*/
|
||||
LearningProcessDate = global.REMOVE_TIME(bot.simulationState.tradingEngine.current.episode.processDate.value)
|
||||
|
||||
if (checkStopTaskGracefully() === false) { break }
|
||||
if (checkStopProcessing() === false) { break }
|
||||
|
||||
bot.TRADING_SESSIONHeartBeat(undefined, undefined, 'Waking up')
|
||||
if (await processDailyFiles() === false) {
|
||||
bot.TRADING_SESSIONHeartBeat(undefined, undefined, 'Waiting for Data Mining to be run')
|
||||
callBackFunction(global.DEFAULT_RETRY_RESPONSE)
|
||||
return
|
||||
}
|
||||
/*
|
||||
With all the indicators data files loaded, we will build the chart object
|
||||
data structure that will be used in user-defied conditions and formulas.
|
||||
*/
|
||||
buildCharts(chart)
|
||||
/*
|
||||
The process of generating the output includes the trading simulation.
|
||||
*/
|
||||
if (checkThereAreCandles(chart) === true) {
|
||||
bot.TRADING_SESSIONHeartBeat(undefined, undefined, 'Running')
|
||||
await generateOutput(chart)
|
||||
bot.TRADING_SESSIONHeartBeat(undefined, undefined, 'Saving')
|
||||
await writeProcessFiles()
|
||||
bot.TRADING_SESSIONHeartBeat(undefined, undefined, 'Sleeping')
|
||||
} else {
|
||||
bot.TRADING_SESSIONHeartBeat(undefined, undefined, 'Waiting for Data Mining to be up to date')
|
||||
callBackFunction(global.DEFAULT_RETRY_RESPONSE)
|
||||
return
|
||||
}
|
||||
/*
|
||||
If for any reason the session was stopped, we will break this loop and exit the process.
|
||||
*/
|
||||
if (bot.STOP_SESSION === true) { break }
|
||||
/*
|
||||
When we get to the end of the market, we need to break this process loop in order
|
||||
to let time pass, new information be collected from the exchange, new data built
|
||||
into indicators, and eventually a new execution of this process.
|
||||
*/
|
||||
if (checkStopHeadOfTheMarket() === false) { break }
|
||||
|
||||
}
|
||||
while (true)
|
||||
}
|
||||
|
||||
checkIfSessionMustStop()
|
||||
/*
|
||||
Everything worked as expected. We return an OK code and wait for
|
||||
the Bot Loop to call us again later.
|
||||
*/
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE)
|
||||
|
||||
function checkThereAreCandles(chart) {
|
||||
let sessionParameters = bot.TRADING_SESSION.tradingParameters
|
||||
let propertyName = 'at' + sessionParameters.timeFrame.config.label.replace('-', '')
|
||||
let candles = chart[propertyName].candles
|
||||
|
||||
if (candles.length === 0) {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
function getContextVariables() {
|
||||
try {
|
||||
let thisReport;
|
||||
let statusReport;
|
||||
|
||||
/* We are going to use the start date as beging of market date. */
|
||||
contextVariables.dateBeginOfMarket = global.REMOVE_TIME(bot.TRADING_SESSION.tradingParameters.timeRange.config.initialDatetime)
|
||||
/*
|
||||
Here we get the status report from the bot who knows which is the end of the market.
|
||||
*/
|
||||
statusReport = statusDependencies.reportsByMainUtility.get("Market Ending Point")
|
||||
|
||||
if (statusReport === undefined) { // This means the status report does not exist, that could happen for instance at the begining of a month.
|
||||
logger.write(MODULE_NAME, "[WARN] start -> getContextVariables -> Status Report does not exist. Retrying Later. ");
|
||||
callBackFunction(global.DEFAULT_RETRY_RESPONSE);
|
||||
return
|
||||
}
|
||||
|
||||
if (statusReport.status === "Status Report is corrupt.") {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> getContextVariables -> Can not continue because dependecy Status Report is corrupt. ");
|
||||
callBackFunction(global.DEFAULT_RETRY_RESPONSE);
|
||||
return
|
||||
}
|
||||
|
||||
thisReport = statusReport.file;
|
||||
|
||||
if (thisReport.lastFile === undefined) {
|
||||
logger.write(MODULE_NAME, "[WARN] start -> getContextVariables -> Undefined Last File. -> thisReport = " + JSON.stringify(thisReport));
|
||||
|
||||
let customOK = {
|
||||
result: global.CUSTOM_OK_RESPONSE.result,
|
||||
message: "Dependency not ready."
|
||||
}
|
||||
logger.write(MODULE_NAME, "[WARN] start -> getContextVariables -> customOK = " + customOK.message);
|
||||
callBackFunction(customOK);
|
||||
return
|
||||
}
|
||||
|
||||
contextVariables.dateEndOfMarket = new Date(thisReport.lastFile.valueOf());
|
||||
|
||||
/* Finally we get our own Status Report. */
|
||||
statusReport = statusDependencies.reportsByMainUtility.get("Self Reference")
|
||||
|
||||
if (statusReport === undefined) { // This means the status report does not exist, that could happen for instance at the begining of a month.
|
||||
logger.write(MODULE_NAME, "[WARN] start -> getContextVariables -> Status Report does not exist. Retrying Later. ");
|
||||
callBackFunction(global.DEFAULT_RETRY_RESPONSE);
|
||||
return
|
||||
}
|
||||
|
||||
if (statusReport.status === "Status Report is corrupt.") {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> getContextVariables -> Can not continue because self dependecy Status Report is corrupt. Aborting Process.");
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
return
|
||||
}
|
||||
|
||||
thisReport = statusReport.file;
|
||||
bot.simulationState = thisReport.simulationState;
|
||||
if (bot.simulationState === undefined) { bot.simulationState = {} } // This should happen only when there is no status report
|
||||
|
||||
if (thisReport.lastFile !== undefined) {
|
||||
if (bot.RESUME !== true) {
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> getContextVariables -> Starting from the begining because bot has just started and resume execution was true."); }
|
||||
startFromBegining();
|
||||
return
|
||||
}
|
||||
contextVariables.lastFile = new Date(thisReport.lastFile);
|
||||
return
|
||||
|
||||
} else {
|
||||
/*
|
||||
We are here becuse either:
|
||||
1. There is no status report
|
||||
2. There is no Last File (this happens on Market Files)
|
||||
*/
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> getContextVariables -> Starting from the begining of the market because own status report not found or lastFile was undefined."); }
|
||||
startFromBegining();
|
||||
return
|
||||
}
|
||||
|
||||
function startFromBegining() {
|
||||
contextVariables.lastFile = new Date(contextVariables.dateBeginOfMarket.getUTCFullYear() + "-" + (contextVariables.dateBeginOfMarket.getUTCMonth() + 1) + "-" + contextVariables.dateBeginOfMarket.getUTCDate() + " " + "00:00" + GMT_SECONDS);
|
||||
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> getContextVariables -> startFromBegining -> contextVariables.lastFile = " + contextVariables.lastFile); }
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> getContextVariables -> err = " + err.stack);
|
||||
if (err.message === "Cannot read property 'file' of undefined") {
|
||||
logger.write(MODULE_NAME, "[HINT] start -> getContextVariables -> Check the bot configuration to see if all of its statusDependencies declarations are correct. ");
|
||||
logger.write(MODULE_NAME, "[HINT] start -> getContextVariables -> Dependencies loaded -> keys = " + JSON.stringify(statusDependencies.keys));
|
||||
}
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
}
|
||||
}
|
||||
|
||||
async function processSingleFiles() {
|
||||
dataFiles = new Map();
|
||||
|
||||
for (let dependencyIndex = 0; dependencyIndex < dataDependenciesModule.nodeArray.length; dependencyIndex++) {
|
||||
let dependency = dataDependenciesModule.nodeArray[dependencyIndex];
|
||||
let datasetModule = dataDependenciesModule.dataSetsModulesArray[dependencyIndex];
|
||||
|
||||
if (datasetModule.node.config.codeName !== "Single-File") {
|
||||
continue
|
||||
}
|
||||
|
||||
if (dataDependenciesModule.isItADepenency('atAnyTimeFrame', datasetModule.node.parentNode.config.singularVariableName) !== true) {
|
||||
continue
|
||||
}
|
||||
|
||||
let fileName = "Data.json";
|
||||
let filePath = datasetModule.node.parentNode.config.codeName + '/' + datasetModule.node.config.codeName;
|
||||
|
||||
/* We cut the async calls via callBacks at this point, so as to have a clearer code upstream */
|
||||
let response = await asyncGetDatasetFile(datasetModule, filePath, fileName)
|
||||
|
||||
if (response.err.result !== global.DEFAULT_OK_RESPONSE.result) {
|
||||
throw (response.err)
|
||||
}
|
||||
|
||||
let dataFile = JSON.parse(response.text);
|
||||
dataFiles.set(dependency.id, dataFile);
|
||||
}
|
||||
|
||||
let mapKey = "Single Files"
|
||||
multiPeriodDataFiles.set(mapKey, dataFiles)
|
||||
}
|
||||
|
||||
async function processMarketFiles() {
|
||||
/*
|
||||
We do market files first since if the simulation is run on daily files, there will
|
||||
be a loop to get each of those files and we do not need that loop to reload market files.
|
||||
|
||||
We will iterate through all posible timeFrames.
|
||||
*/
|
||||
for (let n = 0; n < global.marketFilesPeriods.length; n++) {
|
||||
const timeFrame = global.marketFilesPeriods[n][0];
|
||||
const timeFrameLabel = global.marketFilesPeriods[n][1];
|
||||
|
||||
dataFiles = new Map();
|
||||
|
||||
if (bot.TRADING_SESSION.tradingParameters.timeFrame.config.label === timeFrameLabel) {
|
||||
currentTimeFrame = global.marketFilesPeriods[n][0];
|
||||
currentTimeFrameLabel = global.marketFilesPeriods[n][1];
|
||||
}
|
||||
for (let dependencyIndex = 0; dependencyIndex < dataDependenciesModule.nodeArray.length; dependencyIndex++) {
|
||||
let dependency = dataDependenciesModule.nodeArray[dependencyIndex];
|
||||
let datasetModule = dataDependenciesModule.dataSetsModulesArray[dependencyIndex];
|
||||
|
||||
if (dependency.referenceParent.config.codeName !== "Multi-Period-Market") {
|
||||
continue
|
||||
}
|
||||
|
||||
if (dataDependenciesModule.isItADepenency(timeFrameLabel, datasetModule.node.parentNode.config.singularVariableName) !== true) {
|
||||
if (!(bot.TRADING_SESSION.tradingParameters.timeFrame.config.label === timeFrameLabel && datasetModule.node.parentNode.config.pluralVariableName === 'candles')) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
let fileName = "Data.json";
|
||||
let filePath = dependency.referenceParent.parentNode.config.codeName + '/' + dependency.referenceParent.config.codeName + "/" + timeFrameLabel;
|
||||
|
||||
/* We cut the async calls via callBacks at this point, so as to have a clearer code upstream */
|
||||
let response = await asyncGetDatasetFile(datasetModule, filePath, fileName)
|
||||
|
||||
if (response.err.message === 'File does not exist.') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (response.err.result !== global.DEFAULT_OK_RESPONSE.result) {
|
||||
throw (response.err)
|
||||
}
|
||||
|
||||
let dataFile = JSON.parse(response.text);
|
||||
let trimmedDataFile = trimDataFile(dataFile, datasetModule.node.parentNode.record)
|
||||
dataFiles.set(dependency.id, trimmedDataFile);
|
||||
|
||||
function trimDataFile(dataFile, recordDefinition) {
|
||||
/*
|
||||
Here we will discard all the records in a file that are outside of the current time range.
|
||||
We will include the las element previous to the begining of the time range. This is needed
|
||||
because during the simulation, the current period is not the open one, but the previous to
|
||||
the open, and if we do not include the previous to the initial datetime there will be no
|
||||
current objects at the begining of the simulation for many time frames.
|
||||
*/
|
||||
let beginIndex
|
||||
let endIndex
|
||||
let result = []
|
||||
for (let i = 0; i < recordDefinition.properties.length; i++) {
|
||||
let property = recordDefinition.properties[i]
|
||||
if (property.config.codeName === 'begin') {
|
||||
beginIndex = i
|
||||
}
|
||||
if (property.config.codeName === 'end') {
|
||||
endIndex = i
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < dataFile.length; i++) {
|
||||
let dataRecord = dataFile[i]
|
||||
let begin = dataRecord[beginIndex]
|
||||
let end = dataRecord[endIndex]
|
||||
if (end + timeFrame < bot.TRADING_SESSION.tradingParameters.timeRange.config.initialDatetime - 1) { continue } // /1 because we need the previous closed element
|
||||
if (begin > bot.TRADING_SESSION.tradingParameters.timeRange.config.finalDatetime) { continue }
|
||||
result.push(dataRecord)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
let mapKey = global.marketFilesPeriods[n][1];
|
||||
multiPeriodDataFiles.set(mapKey, dataFiles)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
async function processDailyFiles() {
|
||||
/* Telling the world we are alive and doing well and which date we are processing right now. */
|
||||
let processingDateString = LearningProcessDate.getUTCFullYear() + '-' + utilities.pad(LearningProcessDate.getUTCMonth() + 1, 2) + '-' + utilities.pad(LearningProcessDate.getUTCDate(), 2);
|
||||
bot.processHeartBeat(processingDateString, undefined, "Running...")
|
||||
|
||||
/*
|
||||
We will iterate through all posible timeFrames.
|
||||
*/
|
||||
for (let n = 0; n < global.dailyFilePeriods.length; n++) {
|
||||
const timeFrame = global.dailyFilePeriods[n][0];
|
||||
const timeFrameLabel = global.dailyFilePeriods[n][1];
|
||||
|
||||
if (processConfig.framework.validtimeFrames !== undefined) {
|
||||
let validPeriod = false;
|
||||
for (let i = 0; i < processConfig.framework.validtimeFrames.length; i++) {
|
||||
let period = processConfig.framework.validtimeFrames[i];
|
||||
if (period === timeFrameLabel) { validPeriod = true }
|
||||
}
|
||||
if (validPeriod === false) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if (bot.TRADING_SESSION.tradingParameters.timeFrame.config.label === timeFrameLabel) {
|
||||
currentTimeFrame = global.dailyFilePeriods[n][0];
|
||||
currentTimeFrameLabel = global.dailyFilePeriods[n][1];
|
||||
}
|
||||
|
||||
dataFiles = new Map();
|
||||
|
||||
/*
|
||||
We will iterate through all dependencies, in order to load the
|
||||
files that later will end up at the chart data structure.
|
||||
*/
|
||||
for (let dependencyIndex = 0; dependencyIndex < dataDependenciesModule.nodeArray.length; dependencyIndex++) {
|
||||
let dependency = dataDependenciesModule.nodeArray[dependencyIndex];
|
||||
let datasetModule = dataDependenciesModule.dataSetsModulesArray[dependencyIndex];
|
||||
|
||||
if (dependency.referenceParent.config.codeName !== "Multi-Period-Daily") {
|
||||
continue
|
||||
}
|
||||
|
||||
if (dataDependenciesModule.isItADepenency(timeFrameLabel, datasetModule.node.parentNode.config.singularVariableName) !== true) {
|
||||
if (!(bot.TRADING_SESSION.tradingParameters.timeFrame.config.label === timeFrameLabel && datasetModule.node.parentNode.config.pluralVariableName === 'candles')) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
let dateForPath = LearningProcessDate.getUTCFullYear() + '/' + utilities.pad(LearningProcessDate.getUTCMonth() + 1, 2) + '/' + utilities.pad(LearningProcessDate.getUTCDate(), 2);
|
||||
let filePath
|
||||
if (dependency.referenceParent.config.codeName === "Multi-Period-Daily") {
|
||||
filePath = dependency.referenceParent.parentNode.config.codeName + '/' + dependency.referenceParent.config.codeName + "/" + timeFrameLabel + "/" + dateForPath;
|
||||
} else {
|
||||
filePath = dependency.referenceParent.parentNode.config.codeName + '/' + dependency.referenceParent.config.codeName + "/" + dateForPath;
|
||||
}
|
||||
let fileName = "Data.json";
|
||||
|
||||
/* We cut the async calls via callBacks at this point, so as to have a clearer code upstream */
|
||||
let response = await asyncGetDatasetFile(datasetModule, filePath, fileName)
|
||||
|
||||
if (response.err.message === 'File does not exist.') {
|
||||
return false
|
||||
}
|
||||
if (response.err.result !== global.DEFAULT_OK_RESPONSE.result) {
|
||||
throw (response.err)
|
||||
}
|
||||
|
||||
let dataFile = JSON.parse(response.text);
|
||||
dataFiles.set(dependency.id, dataFile);
|
||||
}
|
||||
|
||||
let mapKey = global.dailyFilePeriods[n][1];
|
||||
multiPeriodDataFiles.set(mapKey, dataFiles)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function checkStopHeadOfTheMarket() {
|
||||
/*
|
||||
We need to check if we have reached the head of the market in order to know
|
||||
when to break the Daily Files Process loop and give time for a new candles /
|
||||
indicators to be built and continue the processing once this process is called
|
||||
again.
|
||||
*/
|
||||
if (bot.simulationState.tradingEngine.current.episode.headOfTheMarket.value === true) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function checkStopTaskGracefully() {
|
||||
/* Validation that we dont need to stop. */
|
||||
if (global.STOP_TASK_GRACEFULLY === true) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function checkStopProcessing() {
|
||||
/* Validation that we dont need to stop. */
|
||||
if (bot.STOP_SESSION === true) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function buildCharts(chart) {
|
||||
const COMMONS = require('../Commons.js')
|
||||
let commons = COMMONS.newCommons(bot, logger, UTILITIES, FILE_STORAGE)
|
||||
|
||||
let mainDependency = {}
|
||||
|
||||
/* The first phase here is about checking that we have everything we need at the definition level. */
|
||||
let dataDependencies = global.NODE_BRANCH_TO_ARRAY(bot.processNode.referenceParent.processDependencies, 'Data Dependency')
|
||||
/*
|
||||
We will filter ourt declared dependencies that are not present in the workspace.
|
||||
This will allow the user to have less Data Mines loaded at the workspace
|
||||
that the ones that a Trading Mine depends on.
|
||||
*/
|
||||
dataDependencies = global.FILTER_OUT_NODES_WITHOUT_REFERENCE_PARENT_FROM_NODE_ARRAY(dataDependencies)
|
||||
|
||||
if (commons.validateDataDependencies(dataDependencies, callBackFunction) !== true) { return }
|
||||
|
||||
let outputDatasets = global.NODE_BRANCH_TO_ARRAY(bot.processNode.referenceParent.processOutput, 'Output Dataset')
|
||||
if (commons.validateOutputDatasets(outputDatasets, callBackFunction) !== true) { return }
|
||||
|
||||
/* The second phase is about transforming the inputs into a format that can be used to apply the user defined code. */
|
||||
for (let j = 0; j < global.marketFilesPeriods.length; j++) {
|
||||
let timeFrameLabel = marketFilesPeriods[j][1]
|
||||
let dataFiles = multiPeriodDataFiles.get(timeFrameLabel)
|
||||
let products = {}
|
||||
|
||||
if (dataFiles !== undefined) {
|
||||
commons.inflateDatafiles(dataFiles, dataDependencies, products, mainDependency, currentTimeFrame)
|
||||
|
||||
let propertyName = 'at' + timeFrameLabel.replace('-', '')
|
||||
chart[propertyName] = products
|
||||
}
|
||||
}
|
||||
|
||||
for (let j = 0; j < global.dailyFilePeriods.length; j++) {
|
||||
let timeFrameLabel = global.dailyFilePeriods[j][1]
|
||||
let dataFiles = multiPeriodDataFiles.get(timeFrameLabel)
|
||||
let products = {}
|
||||
|
||||
if (dataFiles !== undefined) {
|
||||
commons.inflateDatafiles(dataFiles, dataDependencies, products, mainDependency, currentTimeFrame)
|
||||
|
||||
let propertyName = 'at' + timeFrameLabel.replace('-', '')
|
||||
chart[propertyName] = products
|
||||
}
|
||||
}
|
||||
|
||||
/* Single Files */
|
||||
let dataFiles = multiPeriodDataFiles.get('Single Files')
|
||||
let products = {}
|
||||
|
||||
if (dataFiles !== undefined) {
|
||||
commons.inflateDatafiles(dataFiles, dataDependencies, products, mainDependency, currentTimeFrame)
|
||||
|
||||
let propertyName = 'atAnyTimeFrame'
|
||||
chart[propertyName] = products
|
||||
}
|
||||
}
|
||||
|
||||
async function generateOutput(chart) {
|
||||
await algorithmModule.start(
|
||||
chart,
|
||||
currentTimeFrame,
|
||||
currentTimeFrameLabel,
|
||||
LearningProcessDate
|
||||
)
|
||||
|
||||
/*
|
||||
From here on, all other loops executions wont be the first execution and also
|
||||
we will consider that it is not resuming a previous execution as well.
|
||||
*/
|
||||
bot.FIRST_EXECUTION = false
|
||||
bot.RESUME = false
|
||||
}
|
||||
|
||||
async function writeProcessFiles() {
|
||||
let outputDatasets = global.NODE_BRANCH_TO_ARRAY(bot.processNode.referenceParent.processOutput, 'Output Dataset')
|
||||
|
||||
await writeTimeFramesFiles(currentTimeFrame, currentTimeFrameLabel)
|
||||
await writeDataRanges()
|
||||
|
||||
if (currentTimeFrame > global.dailyFilePeriods[0][0]) {
|
||||
await writeMarketStatusReport(currentTimeFrameLabel)
|
||||
} else {
|
||||
await writeDailyStatusReport(currentTimeFrameLabel)
|
||||
}
|
||||
|
||||
async function writeDataRanges() {
|
||||
for (
|
||||
let outputDatasetIndex = 0;
|
||||
outputDatasetIndex < outputDatasets.length;
|
||||
outputDatasetIndex++
|
||||
) {
|
||||
let productCodeName = outputDatasets[outputDatasetIndex].referenceParent.parentNode.config.codeName;
|
||||
await writeDataRange(productCodeName);
|
||||
}
|
||||
|
||||
async function writeDataRange(productCodeName) {
|
||||
let dataRange = {
|
||||
begin: contextVariables.dateBeginOfMarket.valueOf(),
|
||||
end: LearningProcessDate.valueOf() + global.ONE_DAY_IN_MILISECONDS
|
||||
}
|
||||
|
||||
let fileContent = JSON.stringify(dataRange);
|
||||
let fileName = '/Data.Range.json';
|
||||
let filePath = bot.filePathRoot + "/Output/" + bot.TRADING_SESSION.folderName + "/" + productCodeName + "/" + 'Multi-Period-Daily' + fileName;
|
||||
|
||||
let response = await fileStorage.asyncCreateTextFile(filePath, fileContent + '\n')
|
||||
|
||||
if (response.err.result !== global.DEFAULT_OK_RESPONSE.result) {
|
||||
throw (response.err)
|
||||
}
|
||||
|
||||
/*
|
||||
Raise the event that the Data Range was Updated.
|
||||
*/
|
||||
let key = bot.dataMine + "-" + bot.codeName + "-" + productCodeName + "-" + bot.exchange + "-" + bot.market.baseAsset + '/' + bot.market.quotedAsset
|
||||
let event = {
|
||||
dateRange: dataRange
|
||||
}
|
||||
|
||||
global.EVENT_SERVER_CLIENT.raiseEvent(key, 'Data Range Updated', event)
|
||||
}
|
||||
}
|
||||
|
||||
async function writeTimeFramesFiles(currentTimeFrame, currentTimeFrameLabel) {
|
||||
|
||||
for (
|
||||
let outputDatasetIndex = 0;
|
||||
outputDatasetIndex < outputDatasets.length;
|
||||
outputDatasetIndex++
|
||||
) {
|
||||
let productCodeName = outputDatasets[outputDatasetIndex].referenceParent.parentNode.config.codeName;
|
||||
await writeTimeFramesFile(currentTimeFrameLabel, productCodeName, 'Multi-Period-Daily')
|
||||
await writeTimeFramesFile(currentTimeFrameLabel, productCodeName, 'Multi-Period-Market')
|
||||
|
||||
async function writeTimeFramesFile(currentTimeFrameLabel, productCodeName, processType) {
|
||||
|
||||
let timeFramesArray = []
|
||||
timeFramesArray.push(currentTimeFrameLabel)
|
||||
|
||||
let fileContent = JSON.stringify(timeFramesArray)
|
||||
let fileName = '/Time.Frames.json';
|
||||
|
||||
let filePath = bot.filePathRoot + "/Output/" + bot.TRADING_SESSION.folderName + "/" + productCodeName + "/" + processType + fileName;
|
||||
|
||||
let response = await fileStorage.asyncCreateTextFile(filePath, fileContent + '\n')
|
||||
if (response.err.result !== global.DEFAULT_OK_RESPONSE.result) {
|
||||
throw (response.err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function writeDailyStatusReport(currentTimeFrameLabel) {
|
||||
let reportKey = bot.dataMine + "-" + bot.codeName + "-" + bot.processNode.referenceParent.config.codeName
|
||||
let thisReport = statusDependencies.statusReports.get(reportKey)
|
||||
|
||||
thisReport.file.lastExecution = bot.currentDaytime
|
||||
thisReport.file.lastFile = LearningProcessDate
|
||||
thisReport.file.simulationState = bot.simulationState
|
||||
thisReport.file.timeFrames = currentTimeFrameLabel
|
||||
await thisReport.asyncSave()
|
||||
}
|
||||
|
||||
async function writeMarketStatusReport(currentTimeFrameLabel) {
|
||||
let reportKey = bot.dataMine + "-" + bot.codeName + "-" + bot.processNode.referenceParent.config.codeName
|
||||
let thisReport = statusDependencies.statusReports.get(reportKey);
|
||||
|
||||
thisReport.file.lastExecution = bot.processDatetime
|
||||
thisReport.file.simulationState = bot.simulationState
|
||||
thisReport.file.timeFrames = currentTimeFrameLabel
|
||||
|
||||
logger.newInternalLoop(bot.codeName, bot.process, LearningProcessDate)
|
||||
await thisReport.asyncSave()
|
||||
}
|
||||
}
|
||||
|
||||
function checkIfSessionMustStop() {
|
||||
|
||||
if (bot.TRADING_SESSION.type === 'Backtesting Session') {
|
||||
/*
|
||||
Backtests needs only one execution of this process to complete.
|
||||
*/
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[IMPORTANT] checkIfSessionMustStop -> Backtesting Session Finished. Stopping the Session now. ') }
|
||||
bot.TRADING_SESSION.stop('Backtesting Session Finished.')
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
/* An unhandled exception occured. in this case we return Fail and log the stack. */
|
||||
if (err.stack) {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> Unhandled Exception. Will Abort this process. err = " + err.stack);
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
return
|
||||
}
|
||||
|
||||
/* Some expected file was not found. We will return a RETRY code and move on. */
|
||||
if ((err.result === "Fail Because" && err.message === "File does not exist.") || err.code === 'The specified key does not exist.') {
|
||||
logger.write(MODULE_NAME, "[ERROR] File not Found. Will Retry the Process Loop.")
|
||||
callBackFunction(global.DEFAULT_RETRY_RESPONSE);
|
||||
return
|
||||
}
|
||||
|
||||
/* Some other handled exception occured. We return Fail and move on. */
|
||||
if (err.result !== global.DEFAULT_OK_RESPONSE.result) {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> Handled Exception. Will Abort this process. err = " + err.message);
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function pad(str, max) {
|
||||
str = str.toString();
|
||||
return str.length < max ? pad(" " + str, max) : str;
|
||||
}
|
||||
|
||||
async function asyncGetDatasetFile(datasetModule, filePath, fileName) {
|
||||
|
||||
let promise = new Promise((resolve, reject) => {
|
||||
|
||||
datasetModule.getTextFile(filePath, fileName, onFileReceived);
|
||||
function onFileReceived(err, text) {
|
||||
|
||||
let response = {
|
||||
err: err,
|
||||
text: text
|
||||
}
|
||||
resolve(response)
|
||||
}
|
||||
});
|
||||
|
||||
return promise
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
exports.newMarkovDecisionProcess = function newMarkovDecisionProcess(bot, logger) {
|
||||
/*
|
||||
We call the Trading Engine to the data structure that is needed in order to exevute the
|
||||
trading protocol with the specific rules defined at the Trading System.
|
||||
*/
|
||||
const MODULE_NAME = 'Markov Decision Process'
|
||||
let thisObject = {
|
||||
mantain: mantain,
|
||||
reset: reset,
|
||||
getNodeById: getNodeById,
|
||||
cloneValues: cloneValues,
|
||||
setCurrentCandle: setCurrentCandle,
|
||||
setCurrentCycle: setCurrentCycle,
|
||||
initializeNode: initializeNode,
|
||||
initialize: initialize,
|
||||
finalize: finalize
|
||||
}
|
||||
|
||||
let tradingEngine
|
||||
let sessionParameters
|
||||
let nodesMap = new Map()
|
||||
|
||||
return thisObject
|
||||
|
||||
function initialize() {
|
||||
|
||||
tradingEngine = bot.simulationState.tradingEngine
|
||||
sessionParameters = bot.LEARNING_SESSION.learningParameters
|
||||
|
||||
initializeNodeMap(tradingEngine)
|
||||
|
||||
if (bot.FIRST_EXECUTION === true) {
|
||||
/*
|
||||
Here we will go through all the nodes in the Trading Engine hiriarchy and
|
||||
apply the initial value to the value property when needed
|
||||
*/
|
||||
initializeNode(tradingEngine)
|
||||
}
|
||||
}
|
||||
|
||||
function finalize() {
|
||||
tradingEngine = undefined
|
||||
sessionParameters = undefined
|
||||
nodesMap = undefined
|
||||
}
|
||||
|
||||
function mantain() {
|
||||
}
|
||||
|
||||
function reset() {
|
||||
|
||||
}
|
||||
|
||||
function getNodeById(NodeId) {
|
||||
return nodesMap.get(NodeId)
|
||||
}
|
||||
|
||||
function setCurrentCandle(candle) {
|
||||
tradingEngine.current.episode.candle.begin.value = candle.begin
|
||||
tradingEngine.current.episode.candle.end.value = candle.end
|
||||
tradingEngine.current.episode.candle.open.value = candle.open
|
||||
tradingEngine.current.episode.candle.close.value = candle.close
|
||||
tradingEngine.current.episode.candle.min.value = candle.min
|
||||
tradingEngine.current.episode.candle.max.value = candle.max
|
||||
}
|
||||
|
||||
function setCurrentCycle(cycle) {
|
||||
tradingEngine.current.episode.cycle.value = cycle
|
||||
tradingEngine.current.episode.cycle.lastBegin.value = tradingEngine.current.episode.cycle.begin.value
|
||||
tradingEngine.current.episode.cycle.lastEnd.value = tradingEngine.current.episode.cycle.end.value
|
||||
switch (cycle) {
|
||||
case 'First': {
|
||||
tradingEngine.current.episode.cycle.begin.value =
|
||||
tradingEngine.current.episode.candle.begin.value +
|
||||
sessionParameters.timeFrame.config.value
|
||||
tradingEngine.current.episode.cycle.end.value =
|
||||
tradingEngine.current.episode.candle.begin.value +
|
||||
sessionParameters.timeFrame.config.value +
|
||||
sessionParameters.timeFrame.config.value / 2 - 1
|
||||
break
|
||||
}
|
||||
case 'Second': {
|
||||
tradingEngine.current.episode.cycle.begin.value =
|
||||
tradingEngine.current.episode.candle.begin.value +
|
||||
sessionParameters.timeFrame.config.value +
|
||||
sessionParameters.timeFrame.config.value / 2
|
||||
tradingEngine.current.episode.cycle.end.value =
|
||||
tradingEngine.current.episode.candle.begin.value +
|
||||
sessionParameters.timeFrame.config.value +
|
||||
sessionParameters.timeFrame.config.value * 4 / 4 - 1
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function cloneValues(originNode, destinationNode) {
|
||||
if (originNode === undefined) { return }
|
||||
if (destinationNode === undefined) { return }
|
||||
|
||||
/* Here copy the node value */
|
||||
destinationNode.value = originNode.value
|
||||
|
||||
/* Now we go down through all the children of the origin node, assuming the destination node has the same children structure*/
|
||||
let nodeDefinition = global.APP_SCHEMA_MAP.get(originNode.type)
|
||||
if (nodeDefinition === undefined) { return }
|
||||
|
||||
if (nodeDefinition.properties !== undefined) {
|
||||
let previousPropertyName // Since there are cases where there are many properties with the same name,because they can hold nodes of different types but only one at the time, we have to avoind counting each property of those as individual children.
|
||||
for (let i = 0; i < nodeDefinition.properties.length; i++) {
|
||||
let property = nodeDefinition.properties[i]
|
||||
|
||||
switch (property.type) {
|
||||
case 'node': {
|
||||
if (property.name !== previousPropertyName) {
|
||||
if (originNode[property.name] !== undefined && destinationNode[property.name] !== undefined) {
|
||||
cloneValues(originNode[property.name], destinationNode[property.name])
|
||||
}
|
||||
previousPropertyName = property.name
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'array': {
|
||||
if (originNode[property.name] !== undefined && destinationNode[property.name] !== undefined) {
|
||||
let originNodePropertyArray = originNode[property.name]
|
||||
let destinationNodePropertyArray = destinationNode[property.name]
|
||||
for (let m = 0; m < originNodePropertyArray.length; m++) {
|
||||
cloneValues(originNodePropertyArray[m], destinationNodePropertyArray[m])
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function initializeNodeMap(node) {
|
||||
if (node === undefined) { return }
|
||||
/* Add to Node Map */
|
||||
nodesMap.set(node.id, node)
|
||||
|
||||
/* Now we go down through all this node children */
|
||||
let nodeDefinition = global.APP_SCHEMA_MAP.get(node.type)
|
||||
if (nodeDefinition === undefined) { return }
|
||||
|
||||
if (nodeDefinition.properties !== undefined) {
|
||||
let previousPropertyName // Since there are cases where there are many properties with the same name,because they can hold nodes of different types but only one at the time, we have to avoind counting each property of those as individual children.
|
||||
for (let i = 0; i < nodeDefinition.properties.length; i++) {
|
||||
let property = nodeDefinition.properties[i]
|
||||
|
||||
switch (property.type) {
|
||||
case 'node': {
|
||||
if (property.name !== previousPropertyName) {
|
||||
if (node[property.name] !== undefined) {
|
||||
initializeNodeMap(node[property.name])
|
||||
}
|
||||
previousPropertyName = property.name
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'array': {
|
||||
if (node[property.name] !== undefined) {
|
||||
let nodePropertyArray = node[property.name]
|
||||
for (let m = 0; m < nodePropertyArray.length; m++) {
|
||||
initializeNodeMap(nodePropertyArray[m])
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function initializeNode(node) {
|
||||
if (node === undefined) { return }
|
||||
|
||||
/* Here we initialize the node value */
|
||||
if (node.config !== undefined) {
|
||||
if (node.config.initialValue !== undefined) {
|
||||
node.value = node.config.initialValue
|
||||
}
|
||||
}
|
||||
|
||||
/* Now we go down through all this node children */
|
||||
let nodeDefinition = global.APP_SCHEMA_MAP.get(node.type)
|
||||
if (nodeDefinition === undefined) { return }
|
||||
|
||||
if (nodeDefinition.properties !== undefined) {
|
||||
let previousPropertyName // Since there are cases where there are many properties with the same name,because they can hold nodes of different types but only one at the time, we have to avoind counting each property of those as individual children.
|
||||
for (let i = 0; i < nodeDefinition.properties.length; i++) {
|
||||
let property = nodeDefinition.properties[i]
|
||||
|
||||
switch (property.type) {
|
||||
case 'node': {
|
||||
if (property.name !== previousPropertyName) {
|
||||
if (node[property.name] !== undefined) {
|
||||
initializeNode(node[property.name])
|
||||
}
|
||||
previousPropertyName = property.name
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'array': {
|
||||
if (node[property.name] !== undefined) {
|
||||
let nodePropertyArray = node[property.name]
|
||||
for (let m = 0; m < nodePropertyArray.length; m++) {
|
||||
initializeNode(nodePropertyArray[m])
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,646 @@
|
||||
exports.newRoot = function newRoot() {
|
||||
|
||||
/* This is the Execution Datetime */
|
||||
|
||||
global.EXECUTION_DATETIME = new Date();
|
||||
|
||||
/* Time Frames Definitions. */
|
||||
|
||||
global.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"' + ']' + ']';
|
||||
|
||||
global.marketFilesPeriods = JSON.parse(global.marketFilesPeriods);
|
||||
|
||||
global.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"' + ']' + ']';
|
||||
|
||||
global.dailyFilePeriods = JSON.parse(global.dailyFilePeriods);
|
||||
|
||||
const MODULE_NAME = "Root";
|
||||
const WAIT_TIME_FOR_ALL_PROCESS_INSTANCES_TO_START = 10000 // This avoid a race condition that could happen if one process finished before all the other even started.
|
||||
|
||||
let thisObject = {
|
||||
initialize: initialize,
|
||||
start: start
|
||||
}
|
||||
|
||||
const FULL_LOG = true;
|
||||
|
||||
let logDisplace = "Task Server" + " ";
|
||||
|
||||
return thisObject;
|
||||
|
||||
function initialize(callBackFunction) {
|
||||
|
||||
try {
|
||||
|
||||
/* Global control of logging. */
|
||||
|
||||
global.LOG_CONTROL = {
|
||||
"Status Report": {
|
||||
logInfo: false,
|
||||
logWarnings: false,
|
||||
logErrors: true,
|
||||
logContent: false,
|
||||
intensiveLogging: false
|
||||
},
|
||||
"Dataset": {
|
||||
logInfo: false,
|
||||
logWarnings: false,
|
||||
logErrors: true,
|
||||
logContent: false,
|
||||
intensiveLogging: false
|
||||
},
|
||||
"Process Execution Events": {
|
||||
logInfo: false,
|
||||
logWarnings: false,
|
||||
logErrors: true,
|
||||
logContent: false,
|
||||
intensiveLogging: false
|
||||
},
|
||||
"Process Output": {
|
||||
logInfo: false,
|
||||
logWarnings: false,
|
||||
logErrors: true,
|
||||
logContent: false,
|
||||
intensiveLogging: false
|
||||
}
|
||||
};
|
||||
|
||||
callBackFunction();
|
||||
|
||||
}
|
||||
catch (err) {
|
||||
console.log(logDisplace + "Root : [ERROR] initialize -> err = " + err.stack);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function start(processIndex) {
|
||||
|
||||
try {
|
||||
|
||||
/* Now we will run according to what we see at the config file. */
|
||||
|
||||
/* Small Function to fix numbers into strings in a cool way. */
|
||||
|
||||
function pad(str, max) {
|
||||
str = str.toString();
|
||||
return str.length < max ? pad("0" + str, max) : str;
|
||||
}
|
||||
|
||||
/* Process Loops Declarations. */
|
||||
|
||||
const INDICATOR_BOT_MODULE = require('./IndicatorBot');
|
||||
const SENSOR_BOT = require('./SensorBot');
|
||||
const TRADING_BOT_MODULE = require('./TradingBot');
|
||||
|
||||
let botConfig;
|
||||
let processInstance = global.TASK_NODE.bot.processes[processIndex]
|
||||
|
||||
/* Here we will check if we need to load the configuration and code of the bot from a file or we will take that from the UI. */
|
||||
if (
|
||||
processInstance.referenceParent.config.framework !== undefined &&
|
||||
(processInstance.referenceParent.config.framework.name === 'Multi-Period-Market' ||
|
||||
processInstance.referenceParent.config.framework.name === 'Multi-Period-Daily' ||
|
||||
processInstance.referenceParent.config.framework.name === 'Low-Frequency-Trading-Process')
|
||||
) {
|
||||
botConfig = processInstance.referenceParent.parentNode.config
|
||||
botConfig.definedByUI = true
|
||||
bootingBot(processIndex)
|
||||
return
|
||||
} else {
|
||||
getBotConfigFromFile();
|
||||
return
|
||||
}
|
||||
|
||||
function getBotConfigFromFile() {
|
||||
|
||||
try {
|
||||
|
||||
const FILE_STORAGE = require('./FileStorage.js');
|
||||
let fileStorage = FILE_STORAGE.newFileStorage();
|
||||
|
||||
let filePath = global.TASK_NODE.bot.processes[processIndex].referenceParent.parentNode.parentNode.config.codeName + '/bots/' + processInstance.referenceParent.parentNode.config.repo + '/this.bot.config.json';
|
||||
|
||||
fileStorage.getTextFile(filePath, onFileReceived);
|
||||
|
||||
function onFileReceived(err, text) {
|
||||
|
||||
if (err.result !== global.DEFAULT_OK_RESPONSE.result) {
|
||||
console.log(logDisplace + "Root : [ERROR] start -> getBotConfigFromFile -> onInizialized -> onFileReceived -> err = " + JSON.stringify(err));
|
||||
console.log(logDisplace + "Root : [ERROR] start -> getBotConfigFromFile -> onInizialized -> onFileReceived -> filePath = " + filePath);
|
||||
console.log(logDisplace + "Root : [ERROR] start -> getBotConfigFromFile -> onInizialized -> onFileReceived -> dataMine = " + global.TASK_NODE.bot.processes[processIndex].referenceParent.parentNode.parentNode.config.codeName);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
botConfig = JSON.parse(text);
|
||||
botConfig.repo = global.TASK_NODE.bot.config.repo;
|
||||
bootingBot(processIndex);
|
||||
} catch (err) {
|
||||
console.log(logDisplace + "Root : [ERROR] start -> getBotConfigFromFile -> onInizialized -> onFileReceived -> err = " + err.stack);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.log(logDisplace + "Root : [ERROR] start -> getBotConfigFromFile -> err = " + err.stack);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function bootingBot(processIndex) {
|
||||
|
||||
try {
|
||||
|
||||
botConfig.process = global.TASK_NODE.bot.processes[processIndex].referenceParent.config.codeName
|
||||
botConfig.debug = {};
|
||||
botConfig.processNode = global.TASK_NODE.bot.processes[processIndex]
|
||||
|
||||
/* Logs Mantainance Stuff */
|
||||
botConfig.LOGS_TO_DELETE_QUEUE = []
|
||||
botConfig.DELETE_QUEUE_SIZE = 10 // This number represents how many log files can be at the queue at any point in time, which means how many logs are not still deleted.
|
||||
|
||||
/* Simplifying the access to basic info */
|
||||
botConfig.dataMine = global.TASK_NODE.bot.processes[processIndex].referenceParent.parentNode.parentNode.config.codeName
|
||||
botConfig.mineType = global.TASK_NODE.bot.processes[processIndex].referenceParent.parentNode.parentNode.type.replace(' ', '-')
|
||||
botConfig.project = global.TASK_NODE.bot.processes[processIndex].referenceParent.parentNode.parentNode.project
|
||||
botConfig.exchange = global.TASK_NODE.parentNode.parentNode.parentNode.referenceParent.parentNode.parentNode.name
|
||||
botConfig.exchangeNode = global.TASK_NODE.parentNode.parentNode.parentNode.referenceParent.parentNode.parentNode
|
||||
botConfig.market = {
|
||||
baseAsset: global.TASK_NODE.parentNode.parentNode.parentNode.referenceParent.baseAsset.referenceParent.config.codeName,
|
||||
quotedAsset: global.TASK_NODE.parentNode.parentNode.parentNode.referenceParent.quotedAsset.referenceParent.config.codeName
|
||||
}
|
||||
botConfig.uiStartDate = global.TASK_NODE.bot.config.startDate
|
||||
botConfig.config = global.TASK_NODE.bot.config
|
||||
|
||||
/* Loop Counter */
|
||||
botConfig.loopCounter = 0;
|
||||
|
||||
/* File Path Root */
|
||||
botConfig.filePathRoot = 'Project/' + botConfig.project + "/" + botConfig.mineType + "/" + botConfig.dataMine + "/" + botConfig.codeName + '/' + botConfig.exchange + "/" + botConfig.market.baseAsset + "-" + botConfig.market.quotedAsset
|
||||
|
||||
/* Process Key */
|
||||
botConfig.processKey = global.TASK_NODE.bot.processes[processIndex].name + '-' + global.TASK_NODE.bot.processes[processIndex].type + '-' + global.TASK_NODE.bot.processes[processIndex].id
|
||||
|
||||
/* Bot Type */
|
||||
botConfig.type = global.TASK_NODE.bot.processes[processIndex].referenceParent.parentNode.type
|
||||
|
||||
/* Time Frame Filter */
|
||||
if (global.TASK_NODE.bot.timeFramesFilter !== undefined) {
|
||||
botConfig.dailyTimeFrames = global.TASK_NODE.bot.timeFramesFilter.config.dailyTimeFrames
|
||||
botConfig.marketTimeFrames = global.TASK_NODE.bot.timeFramesFilter.config.marketTimeFrames
|
||||
}
|
||||
|
||||
if (global.TASK_NODE.keyReference !== undefined) {
|
||||
if (global.TASK_NODE.keyReference.referenceParent !== undefined) {
|
||||
let key = global.TASK_NODE.keyReference.referenceParent
|
||||
|
||||
botConfig.KEY = key.config.codeName
|
||||
botConfig.SECRET = key.config.secret
|
||||
}
|
||||
}
|
||||
|
||||
let processConfig = global.TASK_NODE.bot.processes[processIndex].referenceParent.config
|
||||
|
||||
if (processConfig.framework !== undefined) {
|
||||
if (processConfig.framework.name === "Multi-Period-Daily" || processConfig.framework.name === "Multi-Period-Market" || processConfig.framework.name === "Low-Frequency-Trading-Process") {
|
||||
if (processConfig.framework.startDate !== undefined) {
|
||||
processConfig.framework.startDate.resumeExecution = true;
|
||||
if (processConfig.startMode.noTime !== undefined) {
|
||||
if (processConfig.startMode.noTime.run === "true") {
|
||||
if (processConfig.startMode.noTime.beginDatetime !== undefined) {
|
||||
processConfig.framework.startDate.fixedDate = processConfig.startMode.noTime.beginDatetime;
|
||||
processConfig.framework.startDate.resumeExecution = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (processConfig.startMode !== undefined) {
|
||||
if (processConfig.startMode.userDefined !== undefined) {
|
||||
if (processConfig.startMode.userDefined.run === "true") {
|
||||
if (processConfig.startMode.userDefined.beginDatetime !== undefined) {
|
||||
processConfig.framework.startDate.fixedDate = processConfig.startMode.userDefined.beginDatetime;
|
||||
processConfig.framework.startDate.resumeExecution = processConfig.startMode.userDefined.resumeExecution;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
if (processConfig.startMode === undefined) { // Default
|
||||
|
||||
botConfig.hasTheBotJustStarted = true;
|
||||
|
||||
switch (botConfig.type) {
|
||||
case 'Sensor Bot': {
|
||||
runSensorBot(botConfig, processConfig);
|
||||
break;
|
||||
}
|
||||
case 'Indicator Bot': {
|
||||
runIndicatorBot(botConfig, processConfig);
|
||||
break;
|
||||
}
|
||||
case 'Trading Bot': {
|
||||
runTradingBot(botConfig, processConfig);
|
||||
break;
|
||||
}
|
||||
case 'Learning Bot': {
|
||||
runLearningBot(botConfig, processConfig);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
console.log(logDisplace + "Root : [ERROR] start -> bootingBot -> Unexpected bot type. -> botConfig.type = " + botConfig.type);
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (processConfig.startMode.noTime !== undefined) {
|
||||
|
||||
if (processConfig.startMode.noTime.run === "true") {
|
||||
|
||||
if (processConfig.startMode.noTime.resumeExecution === true) {
|
||||
botConfig.hasTheBotJustStarted = false;
|
||||
} else {
|
||||
botConfig.hasTheBotJustStarted = true;
|
||||
}
|
||||
|
||||
switch (botConfig.type) {
|
||||
case 'Sensor Bot': {
|
||||
runSensorBot(botConfig, processConfig);
|
||||
break;
|
||||
}
|
||||
case 'Indicator Bot': {
|
||||
runIndicatorBot(botConfig, processConfig);
|
||||
break;
|
||||
}
|
||||
case 'Trading Bot': {
|
||||
runTradingBot(botConfig, processConfig);
|
||||
break;
|
||||
}
|
||||
case 'Learning Bot': {
|
||||
runLearningBot(botConfig, processConfig);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
console.log(logDisplace + "Root : [ERROR] start -> bootingBot -> Unexpected bot type. -> botConfig.type = " + botConfig.type);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (processConfig.startMode.fixedInterval !== undefined) {
|
||||
|
||||
if (processConfig.startMode.fixedInterval.run === "true") {
|
||||
|
||||
botConfig.runAtFixedInterval = true;
|
||||
botConfig.fixedInterval = processConfig.startMode.fixedInterval.interval;
|
||||
|
||||
switch (botConfig.type) {
|
||||
case 'Sensor Bot': {
|
||||
runSensorBot(botConfig, processConfig);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
console.log(logDisplace + "Root : [ERROR] start -> bootingBot -> Unexpected bot type. -> botConfig.type = " + botConfig.type);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (processConfig.startMode.userDefined !== undefined) {
|
||||
|
||||
if (processConfig.startMode.userDefined.run === "true") {
|
||||
|
||||
botConfig.startMode = "User Defined";
|
||||
|
||||
if (processConfig.startMode.userDefined.resumeExecution === true) {
|
||||
botConfig.hasTheBotJustStarted = false;
|
||||
} else {
|
||||
botConfig.hasTheBotJustStarted = true;
|
||||
}
|
||||
|
||||
switch (botConfig.type) {
|
||||
case 'Trading Bot': {
|
||||
runTradingBot(botConfig, processConfig);
|
||||
break;
|
||||
}
|
||||
case 'Learning Bot': {
|
||||
runLearningBot(botConfig, processConfig);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
console.log(logDisplace + "Root : [ERROR] start -> bootingBot -> Unexpected bot type. -> botConfig.type = " + botConfig.type);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.log(logDisplace + "Root : [ERROR] start -> bootingBot -> Unexpected exception. -> err = " + err.stack);
|
||||
}
|
||||
|
||||
|
||||
function runSensorBot(pBotConfig, pProcessConfig) {
|
||||
|
||||
try {
|
||||
global.TOTAL_PROCESS_INSTANCES_CREATED++
|
||||
|
||||
const DEBUG_MODULE = require(global.ROOT_DIR + 'DebugLog');
|
||||
let logger;
|
||||
|
||||
logger = DEBUG_MODULE.newDebugLog();
|
||||
global.LOGGER_MAP.set('runSensorBot', logger)
|
||||
logger.bot = pBotConfig;
|
||||
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> bootingBot -> runSensorBot -> Entering function."); }
|
||||
|
||||
let sensorBot = SENSOR_BOT.newSensorBot(pBotConfig, logger);
|
||||
sensorBot.initialize(pProcessConfig, onInitializeReady);
|
||||
|
||||
function onInitializeReady(err) {
|
||||
|
||||
if (err.result === global.DEFAULT_OK_RESPONSE.result) {
|
||||
|
||||
sensorBot.run(whenRunFinishes);
|
||||
|
||||
function whenRunFinishes(err) {
|
||||
|
||||
pBotConfig.loopCounter = 0;
|
||||
|
||||
let botId = pBotConfig.dataMine + "." + pBotConfig.codeName + "." + pBotConfig.process;
|
||||
|
||||
if (err.result === global.DEFAULT_OK_RESPONSE.result) {
|
||||
logger.write(MODULE_NAME, "[INFO] start -> bootingBot -> runSensorBot -> onInitializeReady -> whenStartFinishes -> Bot execution finished sucessfully.");
|
||||
logger.write(MODULE_NAME, "[INFO] start -> bootingBot -> runSensorBot -> onInitializeReady -> whenStartFinishes -> Bot Id = " + botId);
|
||||
|
||||
logger.persist();
|
||||
} else {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> bootingBot -> runSensorBot -> onInitializeReady -> whenStartFinishes -> err = " + err.message);
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> bootingBot -> runSensorBot -> onInitializeReady -> whenStartFinishes -> Execution will be stopped. ");
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> bootingBot -> runSensorBot -> onInitializeReady -> whenStartFinishes -> Bye.");
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> bootingBot -> runSensorBot -> onInitializeReady -> whenStartFinishes -> Bot Id = " + botId);
|
||||
|
||||
console.log(logDisplace + "Root : [ERROR] start -> bootingBot -> runSensorBot -> onInitializeReady -> whenStartFinishes -> Bot execution was aborted.");
|
||||
logger.persist();
|
||||
}
|
||||
setTimeout(exitProcessInstance, WAIT_TIME_FOR_ALL_PROCESS_INSTANCES_TO_START)
|
||||
}
|
||||
|
||||
} else {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> bootingBot -> runSensorBot -> onInitializeReady -> err = " + err.message);
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> bootingBot -> runSensorBot -> onInitializeReady -> Bot will not be started. ");
|
||||
console.log(logDisplace + "Root : [ERROR] start -> bootingBot -> runSensorBot -> onInitializeReady -> err = " + err.message);
|
||||
|
||||
logger.persist();
|
||||
setTimeout(exitProcessInstance, WAIT_TIME_FOR_ALL_PROCESS_INSTANCES_TO_START)
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.log(logDisplace + "Root : [ERROR] start -> bootingBot -> runSensorBot -> err = " + err.stack);
|
||||
setTimeout(exitProcessInstance, WAIT_TIME_FOR_ALL_PROCESS_INSTANCES_TO_START)
|
||||
}
|
||||
}
|
||||
|
||||
function runIndicatorBot(pBotConfig, pProcessConfig) {
|
||||
|
||||
try {
|
||||
global.TOTAL_PROCESS_INSTANCES_CREATED++
|
||||
|
||||
const DEBUG_MODULE = require(global.ROOT_DIR + 'DebugLog');
|
||||
let logger;
|
||||
|
||||
logger = DEBUG_MODULE.newDebugLog();
|
||||
global.LOGGER_MAP.set('runIndicatorBot', logger)
|
||||
logger.bot = pBotConfig;
|
||||
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> bootingBot -> runIndicatorBot -> Entering function."); }
|
||||
|
||||
let indicatorBot = INDICATOR_BOT_MODULE.newIndicatorBot(pBotConfig, logger);
|
||||
indicatorBot.initialize(pProcessConfig, onInitializeReady);
|
||||
|
||||
function onInitializeReady(err) {
|
||||
|
||||
if (err.result === global.DEFAULT_OK_RESPONSE.result) {
|
||||
|
||||
indicatorBot.run(whenRunFinishes);
|
||||
|
||||
function whenRunFinishes(err) {
|
||||
|
||||
pBotConfig.loopCounter = 0;
|
||||
|
||||
let botId = pBotConfig.dataMine + "." + pBotConfig.codeName + "." + pBotConfig.process;
|
||||
|
||||
if (err.result === global.DEFAULT_OK_RESPONSE.result) {
|
||||
|
||||
logger.write(MODULE_NAME, "[INFO] start -> bootingBot -> runIndicatorBot -> onInitializeReady -> whenStartFinishes -> Bot execution finished sucessfully.");
|
||||
logger.write(MODULE_NAME, "[INFO] start -> bootingBot -> runIndicatorBot -> onInitializeReady -> whenStartFinishes -> Bot Id = " + botId);
|
||||
|
||||
logger.persist();
|
||||
|
||||
} else {
|
||||
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> bootingBot -> runIndicatorBot -> onInitializeReady -> whenStartFinishes -> err = " + err.message);
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> bootingBot -> runIndicatorBot -> onInitializeReady -> whenStartFinishes -> Execution will be stopped. ");
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> bootingBot -> runIndicatorBot -> onInitializeReady -> whenStartFinishes -> Bye.");
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> bootingBot -> runIndicatorBot -> onInitializeReady -> whenStartFinishes -> Bot Id = " + botId);
|
||||
console.log(logDisplace + "Root : [ERROR] start -> bootingBot -> runIndicatorBot -> onInitializeReady -> whenStartFinishes -> Bot execution finished with errors. Please check the logs.");
|
||||
logger.persist();
|
||||
}
|
||||
setTimeout(exitProcessInstance, WAIT_TIME_FOR_ALL_PROCESS_INSTANCES_TO_START)
|
||||
}
|
||||
|
||||
} else {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> bootingBot -> runIndicatorBot -> onInitializeReady -> err = " + err.message);
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> bootingBot -> runIndicatorBot -> onInitializeReady -> Failed to initialize the bot. ");
|
||||
console.log(logDisplace + "Root : [ERROR] start -> bootingBot -> runIndicatorBot -> onInitializeReady -> err = " + err.message);
|
||||
logger.persist();
|
||||
setTimeout(exitProcessInstance, WAIT_TIME_FOR_ALL_PROCESS_INSTANCES_TO_START)
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.log(logDisplace + "Root : [ERROR] start -> bootingBot -> runIndicatorBot -> err = " + err.stack);
|
||||
setTimeout(exitProcessInstance, WAIT_TIME_FOR_ALL_PROCESS_INSTANCES_TO_START)
|
||||
}
|
||||
}
|
||||
|
||||
function runTradingBot(pBotConfig, pProcessConfig) {
|
||||
|
||||
global.TOTAL_PROCESS_INSTANCES_CREATED++
|
||||
|
||||
try {
|
||||
const DEBUG_MODULE = require(global.ROOT_DIR + 'DebugLog');
|
||||
let logger;
|
||||
|
||||
logger = DEBUG_MODULE.newDebugLog();
|
||||
global.LOGGER_MAP.set('runTradingBot', logger)
|
||||
logger.bot = pBotConfig;
|
||||
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> bootingBot -> runTradingBot -> Entering function."); }
|
||||
|
||||
let tradingBot = TRADING_BOT_MODULE.newTradingBot(pBotConfig, logger);
|
||||
tradingBot.initialize(pProcessConfig, onInitializeReady);
|
||||
|
||||
function onInitializeReady(err) {
|
||||
|
||||
if (err.result === global.DEFAULT_OK_RESPONSE.result) {
|
||||
|
||||
tradingBot.run(whenRunFinishes);
|
||||
|
||||
function whenRunFinishes(err) {
|
||||
|
||||
pBotConfig.loopCounter = 0;
|
||||
|
||||
let botId;
|
||||
|
||||
botId = pBotConfig.dataMine + "." + pBotConfig.codeName + "." + pBotConfig.process;
|
||||
|
||||
if (err.result === global.DEFAULT_OK_RESPONSE.result) {
|
||||
|
||||
logger.write(MODULE_NAME, "[INFO] start -> bootingBot -> runTradingBot -> onInitializeReady -> whenStartFinishes -> Bot execution finished sucessfully.");
|
||||
logger.write(MODULE_NAME, "[INFO] start -> bootingBot -> runTradingBot -> onInitializeReady -> whenStartFinishes -> Bot Id = " + botId);
|
||||
|
||||
logger.persist();
|
||||
|
||||
} else {
|
||||
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> bootingBot -> runTradingBot -> onInitializeReady -> whenStartFinishes -> err = " + err.message);
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> bootingBot -> runTradingBot -> onInitializeReady -> whenStartFinishes -> Execution will be stopped. ");
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> bootingBot -> runTradingBot -> onInitializeReady -> whenStartFinishes -> Bye.");
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> bootingBot -> runTradingBot -> onInitializeReady -> whenStartFinishes -> Bot Id = " + botId);
|
||||
console.log(logDisplace + "Root : [ERROR] start -> bootingBot -> runTradingBot -> onInitializeReady -> whenStartFinishes -> Bot execution finished with errors. Please check the logs.");
|
||||
logger.persist();
|
||||
}
|
||||
setTimeout(exitProcessInstance, WAIT_TIME_FOR_ALL_PROCESS_INSTANCES_TO_START)
|
||||
}
|
||||
|
||||
} else {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> bootingBot -> runTradingBot -> onInitializeReady -> err = " + err.message);
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> bootingBot -> runTradingBot -> onInitializeReady -> Failed to initialize the bot. ");
|
||||
console.log(logDisplace + "Root : [ERROR] start -> bootingBot -> runTradingBot -> onInitializeReady -> err = " + err.message);
|
||||
logger.persist();
|
||||
setTimeout(exitProcessInstance, WAIT_TIME_FOR_ALL_PROCESS_INSTANCES_TO_START)
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.log(logDisplace + "Root : [ERROR] start -> bootingBot -> runTradingBot -> err = " + err.stack);
|
||||
setTimeout(exitProcessInstance, WAIT_TIME_FOR_ALL_PROCESS_INSTANCES_TO_START)
|
||||
}
|
||||
}
|
||||
|
||||
function runLearningBot(pBotConfig, pProcessConfig) {
|
||||
|
||||
global.TOTAL_PROCESS_INSTANCES_CREATED++
|
||||
|
||||
try {
|
||||
const DEBUG_MODULE = require(global.ROOT_DIR + 'DebugLog');
|
||||
let logger;
|
||||
|
||||
logger = DEBUG_MODULE.newDebugLog();
|
||||
global.LOGGER_MAP.set('runLearningBot', logger)
|
||||
logger.bot = pBotConfig;
|
||||
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> bootingBot -> runLearningBot -> Entering function."); }
|
||||
|
||||
let learningBot = TRADING_BOT_MODULE.newLearningBot(pBotConfig, logger);
|
||||
learningBot.initialize(pProcessConfig, onInitializeReady);
|
||||
|
||||
function onInitializeReady(err) {
|
||||
|
||||
if (err.result === global.DEFAULT_OK_RESPONSE.result) {
|
||||
|
||||
learningBot.run(whenRunFinishes);
|
||||
|
||||
function whenRunFinishes(err) {
|
||||
|
||||
pBotConfig.loopCounter = 0;
|
||||
|
||||
let botId;
|
||||
|
||||
botId = pBotConfig.dataMine + "." + pBotConfig.codeName + "." + pBotConfig.process;
|
||||
|
||||
if (err.result === global.DEFAULT_OK_RESPONSE.result) {
|
||||
|
||||
logger.write(MODULE_NAME, "[INFO] start -> bootingBot -> runLearningBot -> onInitializeReady -> whenStartFinishes -> Bot execution finished sucessfully.");
|
||||
logger.write(MODULE_NAME, "[INFO] start -> bootingBot -> runLearningBot -> onInitializeReady -> whenStartFinishes -> Bot Id = " + botId);
|
||||
|
||||
logger.persist();
|
||||
|
||||
} else {
|
||||
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> bootingBot -> runLearningBot -> onInitializeReady -> whenStartFinishes -> err = " + err.message);
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> bootingBot -> runLearningBot -> onInitializeReady -> whenStartFinishes -> Execution will be stopped. ");
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> bootingBot -> runLearningBot -> onInitializeReady -> whenStartFinishes -> Bye.");
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> bootingBot -> runLearningBot -> onInitializeReady -> whenStartFinishes -> Bot Id = " + botId);
|
||||
console.log(logDisplace + "Root : [ERROR] start -> bootingBot -> runLearningBot -> onInitializeReady -> whenStartFinishes -> Bot execution finished with errors. Please check the logs.");
|
||||
logger.persist();
|
||||
}
|
||||
setTimeout(exitProcessInstance, WAIT_TIME_FOR_ALL_PROCESS_INSTANCES_TO_START)
|
||||
}
|
||||
|
||||
} else {
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> bootingBot -> runLearningBot -> onInitializeReady -> err = " + err.message);
|
||||
logger.write(MODULE_NAME, "[ERROR] start -> bootingBot -> runLearningBot -> onInitializeReady -> Failed to initialize the bot. ");
|
||||
console.log(logDisplace + "Root : [ERROR] start -> bootingBot -> runLearningBot -> onInitializeReady -> err = " + err.message);
|
||||
logger.persist();
|
||||
setTimeout(exitProcessInstance, WAIT_TIME_FOR_ALL_PROCESS_INSTANCES_TO_START)
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.log(logDisplace + "Root : [ERROR] start -> bootingBot -> runLearningBot -> err = " + err.stack);
|
||||
setTimeout(exitProcessInstance, WAIT_TIME_FOR_ALL_PROCESS_INSTANCES_TO_START)
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.log(logDisplace + "Root : [ERROR] start -> bootingBot -> err = " + err.stack);
|
||||
setTimeout(exitProcessInstance, WAIT_TIME_FOR_ALL_PROCESS_INSTANCES_TO_START)
|
||||
}
|
||||
}
|
||||
|
||||
function exitProcessInstance() {
|
||||
|
||||
global.ENDED_PROCESSES_COUNTER++
|
||||
//console.log("[INFO] Task Server -> " + global.TASK_NODE.name + " -> exitProcessInstance -> Process #" + global.ENDED_PROCESSES_COUNTER + " from " + global.TOTAL_PROCESS_INSTANCES_CREATED + " exiting.");
|
||||
|
||||
if (global.ENDED_PROCESSES_COUNTER === global.TOTAL_PROCESS_INSTANCES_CREATED) {
|
||||
global.EXIT_NODE_PROCESS()
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("[ERROR] Task Server -> " + global.TASK_NODE.name + " -> Root -> Start -> Err = " + err.stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,595 @@
|
||||
exports.newSensorBot = function newSensorBot(bot, parentLogger) {
|
||||
|
||||
const MODULE_NAME = "Sensor Bot";
|
||||
const FULL_LOG = true;
|
||||
|
||||
let USER_BOT_MODULE;
|
||||
let COMMONS_MODULE;
|
||||
|
||||
const FILE_STORAGE = require('./FileStorage.js');
|
||||
let fileStorage = FILE_STORAGE.newFileStorage(parentLogger);
|
||||
|
||||
const DEBUG_MODULE = require(global.ROOT_DIR + 'DebugLog');
|
||||
let logger; // We need this here in order for the loopHealth function to work and be able to rescue the loop when it gets in trouble.
|
||||
|
||||
let nextLoopTimeoutHandle;
|
||||
|
||||
let thisObject = {
|
||||
initialize: initialize,
|
||||
run: run
|
||||
};
|
||||
|
||||
let processConfig;
|
||||
|
||||
return thisObject;
|
||||
|
||||
function initialize(pProcessConfig, callBackFunction) {
|
||||
/* This function is exactly the same in the 3 modules representing the 2 different bot types loops. */
|
||||
try {
|
||||
processConfig = pProcessConfig;
|
||||
|
||||
if (bot.definedByUI === true) {
|
||||
/* The code of the bot is defined at the UI. No need to load a file with the code. */
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE);
|
||||
return
|
||||
}
|
||||
|
||||
let filePath = bot.dataMine + "/" + "bots" + "/" + bot.processNode.referenceParent.parentNode.config.repo + "/" + pProcessConfig.codeName
|
||||
filePath += "/User.Bot.js"
|
||||
|
||||
fileStorage.getTextFile(filePath, onBotDownloaded);
|
||||
|
||||
function onBotDownloaded(err, text) {
|
||||
if (err.result !== global.DEFAULT_OK_RESPONSE.result) {
|
||||
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] initialize -> onInizialized -> onBotDownloaded -> err = " + err.message);
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] initialize -> onInizialized -> onBotDownloaded -> filePath = " + filePath);
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
return;
|
||||
}
|
||||
USER_BOT_MODULE = {}
|
||||
|
||||
try {
|
||||
USER_BOT_MODULE.newUserBot = eval(text); // TODO This needs to be changed function
|
||||
} catch (err) {
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] initialize -> onBotDownloaded -> err = " + err.stack);
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
}
|
||||
|
||||
filePath = bot.dataMine + "/" + "bots" + "/" + bot.processNode.referenceParent.parentNode.config.repo;
|
||||
filePath += "/Commons.js"
|
||||
|
||||
fileStorage.getTextFile(filePath, onCommonsDownloaded);
|
||||
|
||||
function onCommonsDownloaded(err, text) {
|
||||
if (err.result !== global.DEFAULT_OK_RESPONSE.result) {
|
||||
parentLogger.write(MODULE_NAME, "[WARN] initialize -> onBotDownloaded -> onCommonsDownloaded -> Commons not found: " + err.message);
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE);
|
||||
return;
|
||||
}
|
||||
COMMONS_MODULE = {}
|
||||
|
||||
COMMONS_MODULE.newCommons = eval(text); // TODO This needs to be changed function
|
||||
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] initialize -> err = " + err.stack);
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
}
|
||||
}
|
||||
|
||||
function run(callBackFunction) {
|
||||
try {
|
||||
let fixedTimeLoopIntervalHandle;
|
||||
|
||||
/* Heartbeats sent to the UI */
|
||||
bot.processHeartBeat = processHeartBeat
|
||||
|
||||
if (bot.runAtFixedInterval === true) {
|
||||
fixedTimeLoopIntervalHandle = setInterval(loop, bot.fixedInterval);
|
||||
loop(); // First run.
|
||||
} else {
|
||||
loop();
|
||||
}
|
||||
|
||||
function loop() {
|
||||
try {
|
||||
processHeartBeat(undefined, undefined, "Running...")
|
||||
function pad(str, max) {
|
||||
str = str.toString();
|
||||
return str.length < max ? pad(" " + str, max) : str;
|
||||
}
|
||||
|
||||
/* For each loop we want to create a new log file. */
|
||||
if (logger !== undefined) {
|
||||
logger.finalize()
|
||||
}
|
||||
logger = DEBUG_MODULE.newDebugLog();
|
||||
global.LOGGER_MAP.set(MODULE_NAME, logger)
|
||||
logger.bot = bot;
|
||||
logger.initialize();
|
||||
|
||||
bot.loopCounter++;
|
||||
bot.loopStartTime = new Date().valueOf();
|
||||
|
||||
/* We tell the UI that we are running. */
|
||||
processHeartBeat()
|
||||
|
||||
/* We define here all the modules that the rest of the infraestructure, including the bots themselves can consume. */
|
||||
const UTILITIES = require(global.ROOT_DIR + 'CloudUtilities');
|
||||
const STATUS_REPORT = require(global.ROOT_DIR + 'StatusReport');
|
||||
const STATUS_DEPENDENCIES = require(global.ROOT_DIR + 'StatusDependencies');
|
||||
const PROCESS_EXECUTION_EVENTS = require(global.ROOT_DIR + 'ProcessExecutionEvents');
|
||||
const PROCESS_OUTPUT = require(global.ROOT_DIR + 'ProcessOutput');
|
||||
|
||||
/* We define the datetime for the process that we are running now. This will be the official processing time for both the infraestructure and the bot. */
|
||||
bot.processDatetime = new Date(); // This will be considered the process date and time, so as to have it consistenly all over the execution.
|
||||
|
||||
/* High level log entry */
|
||||
console.log(new Date().toISOString() + " " + pad(bot.exchange, 20) + " " + pad(bot.market.baseAsset + '/' + bot.market.quotedAsset, 10) + " " + pad(bot.codeName, 30) + " " + pad(bot.process, 30)
|
||||
+ " Main Loop # " + pad(Number(bot.loopCounter), 8))
|
||||
|
||||
/* We will prepare first the infraestructure needed for the bot to run. There are 4 modules we need to sucessfullly initialize first. */
|
||||
let processExecutionEvents
|
||||
let userBot;
|
||||
let statusDependencies;
|
||||
let exchangeAPI;
|
||||
|
||||
let nextWaitTime;
|
||||
|
||||
initializeProcessExecutionEvents();
|
||||
|
||||
function initializeProcessExecutionEvents() {
|
||||
try {
|
||||
processExecutionEvents = PROCESS_EXECUTION_EVENTS.newProcessExecutionEvents(bot, logger)
|
||||
processExecutionEvents.initialize(processConfig, onInizialized);
|
||||
|
||||
function onInizialized(err) {
|
||||
try {
|
||||
switch (err.result) {
|
||||
case global.DEFAULT_OK_RESPONSE.result: {
|
||||
logger.write(MODULE_NAME, "[INFO] run -> loop -> initializeProcessExecutionEvents -> onInizialized -> Execution finished well.");
|
||||
startProcessExecutionEvents()
|
||||
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, "[WARN] run -> loop -> initializeProcessExecutionEvents -> onInizialized -> Retry Later. Requesting Execution Retry.");
|
||||
nextWaitTime = 'Retry';
|
||||
loopControl(nextWaitTime);
|
||||
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] run -> loop -> initializeProcessExecutionEvents -> onInizialized -> Operation Failed. Aborting the process.");
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
default: {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeProcessExecutionEvents -> onInizialized -> Unhandled err.result received. -> err.result = " + err.result);
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeProcessExecutionEvents -> onInizialized -> Unhandled err.result received. -> err = " + err.message);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeProcessExecutionEvents -> onInizialized -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeProcessExecutionEvents -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function startProcessExecutionEvents() {
|
||||
try {
|
||||
processExecutionEvents.start(onStarted);
|
||||
|
||||
function onStarted(err) {
|
||||
try {
|
||||
switch (err.result) {
|
||||
case global.DEFAULT_OK_RESPONSE.result: {
|
||||
logger.write(MODULE_NAME, "[INFO] run -> loop -> startProcessExecutionEvents -> onStarted -> Execution finished well.");
|
||||
|
||||
if (global.STOP_TASK_GRACEFULLY === true) {
|
||||
loopControl()
|
||||
return
|
||||
}
|
||||
|
||||
initializeStatusDependencies();
|
||||
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, "[WARN] run -> loop -> startProcessExecutionEvents -> onStarted -> Retry Later. Requesting Execution Retry.");
|
||||
nextWaitTime = 'Retry';
|
||||
loopControl(nextWaitTime);
|
||||
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] run -> loop -> startProcessExecutionEvents -> onStarted -> Operation Failed. Aborting the process.");
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
default: {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> startProcessExecutionEvents -> onStarted -> Unhandled err.result received. -> err.result = " + err.result);
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> startProcessExecutionEvents -> onStarted -> Unhandled err.result received. -> err = " + err.message);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> startProcessExecutionEvents -> onStarted -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> startProcessExecutionEvents -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function initializeStatusDependencies() {
|
||||
try {
|
||||
statusDependencies = STATUS_DEPENDENCIES.newStatusDependencies(bot, logger, STATUS_REPORT, UTILITIES, PROCESS_OUTPUT);
|
||||
statusDependencies.initialize(onInizialized);
|
||||
|
||||
function onInizialized(err) {
|
||||
try {
|
||||
switch (err.result) {
|
||||
case global.DEFAULT_OK_RESPONSE.result: {
|
||||
logger.write(MODULE_NAME, "[INFO] run -> loop -> initializeStatusDependencies -> onInizialized -> Execution finished well.");
|
||||
initializeUserBot();
|
||||
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, "[WARN] run -> loop -> initializeStatusDependencies -> onInizialized -> Retry Later. Requesting Execution Retry.");
|
||||
nextWaitTime = 'Retry';
|
||||
loopControl(nextWaitTime);
|
||||
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] run -> loop -> initializeStatusDependencies -> onInizialized -> Operation Failed. Aborting the process.");
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
default: {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeStatusDependencies -> onInizialized -> Unhandled err.result received. -> err.result = " + err.result);
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeStatusDependencies -> onInizialized -> Unhandled err.result received. -> err = " + err.message);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeStatusDependencies -> onInizialized -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeStatusDependencies -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function initializeUserBot() {
|
||||
try {
|
||||
usertBot = USER_BOT_MODULE.newUserBot(bot, logger, COMMONS_MODULE, UTILITIES, FILE_STORAGE, STATUS_REPORT, exchangeAPI);
|
||||
usertBot.initialize(statusDependencies, onInizialized);
|
||||
|
||||
function onInizialized(err) {
|
||||
try {
|
||||
switch (err.result) {
|
||||
case global.DEFAULT_OK_RESPONSE.result: {
|
||||
logger.write(MODULE_NAME, "[INFO] run -> loop -> initializeUserBot -> onInizialized > Execution finished well.");
|
||||
startUserBot();
|
||||
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, "[WARN] run -> loop -> initializeUserBot -> onInizialized > Retry Later. Requesting Execution Retry.");
|
||||
nextWaitTime = 'Retry';
|
||||
loopControl(nextWaitTime);
|
||||
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] run -> loop -> initializeUserBot -> onInizialized > Operation Failed. Aborting the process.");
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
case global.CUSTOM_OK_RESPONSE.result: {
|
||||
|
||||
switch (err.message) {
|
||||
default: {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeUserBot -> onInizialized > Unhandled custom response received. -> err = " + err.message);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
default: {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeUserBot -> onInizialized > Unhandled err.result received. -> err.result = " + err.result);
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeUserBot -> onInizialized > Unhandled err.result received. -> err = " + err.message);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeUserBot -> onInizialized -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeUserBot -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function startUserBot() {
|
||||
try {
|
||||
usertBot.start(onFinished);
|
||||
|
||||
function onFinished(err) {
|
||||
try {
|
||||
switch (err.result) {
|
||||
case global.DEFAULT_OK_RESPONSE.result: {
|
||||
logger.write(MODULE_NAME, "[INFO] run -> loop -> startUserBot -> onFinished > Execution finished well.");
|
||||
finishProcessExecutionEvents()
|
||||
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, "[WARN] run -> loop -> startUserBot -> onFinished > Retry Later. Requesting Execution Retry.");
|
||||
nextWaitTime = 'Retry';
|
||||
loopControl(nextWaitTime);
|
||||
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] run -> loop -> startUserBot -> onFinished > Operation Failed. Aborting the process.");
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
case global.CUSTOM_OK_RESPONSE.result: {
|
||||
|
||||
switch (err.message) {
|
||||
case "Dependency does not exist.": {
|
||||
logger.write(MODULE_NAME, "[WARN] run -> loop -> startUserBot -> onFinished > Dependency does not exist. This Loop will go to sleep.");
|
||||
nextWaitTime = 'Sleep';
|
||||
loopControl(nextWaitTime);
|
||||
return;
|
||||
}
|
||||
case "Dependency not ready.": {
|
||||
logger.write(MODULE_NAME, "[WARN] run -> loop -> startUserBot -> onFinished > Dependency not ready. This Loop will go to sleep.");
|
||||
nextWaitTime = 'Sleep';
|
||||
loopControl(nextWaitTime);
|
||||
return;
|
||||
}
|
||||
default: {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> startUserBot -> onFinished > Unhandled custom response received. -> err = " + err.message);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
default: {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> startUserBot -> onFinished > Unhandled err.result received. -> err.result = " + err.result);
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> startUserBot -> onFinished > Unhandled err.result received. -> err = " + err.message);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> startUserBot -> onFinished -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> startUserBot -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function finishProcessExecutionEvents() {
|
||||
try {
|
||||
processExecutionEvents.finish(onFinished);
|
||||
|
||||
function onFinished(err) {
|
||||
try {
|
||||
processExecutionEvents.finalize()
|
||||
processExecutionEvents = undefined
|
||||
|
||||
switch (err.result) {
|
||||
case global.DEFAULT_OK_RESPONSE.result: {
|
||||
logger.write(MODULE_NAME, "[INFO] run -> loop -> finishProcessExecutionEvents -> onFinished -> Execution finished well.");
|
||||
nextWaitTime = 'Normal';
|
||||
loopControl(nextWaitTime);
|
||||
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, "[WARN] run -> loop -> finishProcessExecutionEvents -> onFinished -> Retry Later. Requesting Execution Retry.");
|
||||
nextWaitTime = 'Retry';
|
||||
loopControl(nextWaitTime);
|
||||
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] run -> loop -> finishProcessExecutionEvents -> onFinished -> Operation Failed. Aborting the process.");
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
default: {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> finishProcessExecutionEvents -> onFinished -> Unhandled err.result received. -> err.result = " + err.result);
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> finishProcessExecutionEvents -> onFinished -> Unhandled err.result received. -> err = " + err.message);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> finishProcessExecutionEvents -> onFinished -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> finishProcessExecutionEvents -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function loopControl(nextWaitTime) {
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] run -> loop -> loopControl -> nextWaitTime = " + nextWaitTime); }
|
||||
|
||||
/* We show we reached the end of the loop. */
|
||||
/* Here we check if we must stop the loop gracefully. */
|
||||
shallWeStop(onStop, onContinue);
|
||||
|
||||
function onStop() {
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] run -> loop -> loopControl -> onStop -> Stopping the Loop Gracefully. See you next time!"); }
|
||||
processStopped()
|
||||
return;
|
||||
}
|
||||
|
||||
function onContinue() {
|
||||
/* Sensor bots are going to be executed after a configured period of time after the last execution ended. This is to avoid overlapping executions. */
|
||||
switch (nextWaitTime) {
|
||||
case 'Normal': {
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] run -> loop -> loopControl -> Restarting Loop in " + (processConfig.normalWaitTime / 1000) + " seconds."); }
|
||||
nextLoopTimeoutHandle = setTimeout(loop, processConfig.normalWaitTime);
|
||||
processHeartBeat(undefined, undefined, "Waiting " + processConfig.normalWaitTime / 1000 + " seconds for next execution.")
|
||||
if (global.WRITE_LOGS_TO_FILES === 'true') {
|
||||
logger.persist();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'Retry': {
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] run -> loop -> loopControl -> Restarting Loop in " + (processConfig.retryWaitTime / 1000) + " seconds."); }
|
||||
nextLoopTimeoutHandle = setTimeout(loop, processConfig.retryWaitTime);
|
||||
processHeartBeat(undefined, undefined, "Trying to recover from some problem. Waiting " + processConfig.retryWaitTime / 1000 + " seconds for next execution.")
|
||||
logger.persist();
|
||||
}
|
||||
break;
|
||||
case 'Sleep': {
|
||||
if (bot.runAtFixedInterval === true) {
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] run -> loop -> loopControl -> Fixed Interval Sleep exit point reached."); }
|
||||
logger.persist();
|
||||
return;
|
||||
} else {
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] run -> loop -> loopControl -> Restarting Loop in " + (processConfig.sleepWaitTime / 60000) + " minutes."); }
|
||||
nextLoopTimeoutHandle = setTimeout(loop, processConfig.sleepWaitTime);
|
||||
processHeartBeat(undefined, undefined, "Waiting " + processConfig.sleepWaitTime / 60000 + " minutes for next execution.")
|
||||
logger.persist();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'Coma': {
|
||||
if (bot.runAtFixedInterval === true) {
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] run -> loop -> loopControl -> Fixed Interval Coma exit point reached."); }
|
||||
logger.persist();
|
||||
return;
|
||||
} else {
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] run -> loop -> loopControl -> Restarting Loop in " + (processConfig.comaWaitTime / 3600000) + " hours."); }
|
||||
nextLoopTimeoutHandle = setTimeout(loop, processConfig.comaWaitTime);
|
||||
processHeartBeat(undefined, undefined, "Waiting " + processConfig.comaWaitTime / 3600000 + " hours for next execution.")
|
||||
logger.persist();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function shallWeStop(stopCallBack, continueCallBack) {
|
||||
try {
|
||||
/* IMPORTANT: This function is exactly the same on the 3 modules. */
|
||||
if (!global.STOP_TASK_GRACEFULLY) {
|
||||
continueCallBack();
|
||||
} else {
|
||||
stopCallBack();
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> shallWeStop -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] run -> loop -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function processHeartBeat(processingDate, percentage, status) {
|
||||
let event = {
|
||||
seconds: (new Date()).getSeconds(),
|
||||
processingDate: processingDate,
|
||||
percentage: percentage,
|
||||
status: status
|
||||
}
|
||||
global.EVENT_SERVER_CLIENT.raiseEvent(bot.processKey, 'Heartbeat', event)
|
||||
}
|
||||
|
||||
function processStopped() {
|
||||
if (global.unexpectedError !== undefined) {
|
||||
global.PROCESS_ERROR(bot.processKey, undefined, "An unexpected error caused the Process to stop.")
|
||||
} else {
|
||||
global.EVENT_SERVER_CLIENT.raiseEvent(bot.processKey, 'Stopped')
|
||||
}
|
||||
logger.persist();
|
||||
clearInterval(fixedTimeLoopIntervalHandle);
|
||||
clearTimeout(nextLoopTimeoutHandle);
|
||||
if (global.unexpectedError !== undefined) {
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE)
|
||||
} else {
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
catch (err) {
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] run -> err = " + err.stack);
|
||||
clearInterval(fixedTimeLoopIntervalHandle);
|
||||
clearTimeout(nextLoopTimeoutHandle);
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
exports.newSocialBots = function newSocialBots(bot, logger) {
|
||||
|
||||
const MODULE_NAME = 'Social Bots'
|
||||
|
||||
let thisObject = {
|
||||
sendMessage: sendMessage,
|
||||
initialize: initialize,
|
||||
finalize: finalize
|
||||
}
|
||||
|
||||
const TELEGRAM_BOT_MODULE = require('./TelegramBot.js')
|
||||
|
||||
return thisObject
|
||||
|
||||
function initialize() {
|
||||
if (bot.TRADING_SESSION.socialBots !== undefined) {
|
||||
if (bot.TRADING_SESSION.socialBots.bots !== undefined) {
|
||||
for (let i = 0; i < bot.TRADING_SESSION.socialBots.bots.length; i++) {
|
||||
let socialBot = bot.TRADING_SESSION.socialBots.bots[i]
|
||||
if (socialBot.type === "Telegram Bot") {
|
||||
let config = socialBot.config
|
||||
socialBot.botInstance = TELEGRAM_BOT_MODULE.newTelegramBot(bot, logger)
|
||||
socialBot.botInstance.initialize(config.botToken, config.chatId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function announce(text) {
|
||||
if (bot.TRADING_SESSION.socialBots.bots !== undefined) {
|
||||
for (let i = 0; i < bot.TRADING_SESSION.socialBots.bots.length; i++) {
|
||||
let socialBot = bot.TRADING_SESSION.socialBots.bots[i]
|
||||
try {
|
||||
if (socialBot.type === "Telegram Bot") {
|
||||
socialBot.botInstance.telegramAPI.sendMessage(socialBot.botInstance.chatId, text).catch(err => logger.write(MODULE_NAME, "[WARN] initialize -> initializeSocialBots -> announce -> Telegram API error -> err = " + err))
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[WARN] initialize -> announce -> err = " + err.stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bot.TRADING_SESSION.socialBots.announce = announce
|
||||
}
|
||||
}
|
||||
|
||||
function finalize() {
|
||||
if (bot.TRADING_SESSION.socialBots === undefined) { return }
|
||||
if (bot.TRADING_SESSION.socialBots.bots !== undefined) {
|
||||
for (let i = 0; i < bot.TRADING_SESSION.socialBots.bots.length; i++) {
|
||||
let socialBot = bot.TRADING_SESSION.socialBots.bots[i]
|
||||
if (socialBot.type === "Telegram Bot") {
|
||||
socialBot.botInstance.finalize()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sendMessage(message) {
|
||||
if (bot.TRADING_SESSION.socialBots !== undefined) {
|
||||
if (bot.TRADING_SESSION.socialBots.bots !== undefined) {
|
||||
for (let i = 0; i < bot.TRADING_SESSION.socialBots.bots.length; i++) {
|
||||
let socialBot = bot.TRADING_SESSION.socialBots.bots[i]
|
||||
if (socialBot.type === "Telegram Bot") {
|
||||
socialBot.botInstance.sendMessage(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
exports.newStatusDependencies = function newStatusDependencies(BOT, logger, STATUS_REPORT, UTILITIES, PROCESS_OUTPUT) {
|
||||
|
||||
const FULL_LOG = true;
|
||||
const LOG_FILE_CONTENT = false;
|
||||
const MODULE_NAME = "Status Dependencies";
|
||||
|
||||
let bot = BOT
|
||||
|
||||
let thisObject = {
|
||||
nodeArray: undefined,
|
||||
statusReports: new Map(),
|
||||
reportsByMainUtility: new Map(),
|
||||
initialize: initialize,
|
||||
finalize: finalize,
|
||||
keys: []
|
||||
};
|
||||
|
||||
return thisObject;
|
||||
|
||||
function initialize(callBackFunction) {
|
||||
try {
|
||||
/* Basic Valdidations */
|
||||
if (bot.processNode.referenceParent.processDependencies !== undefined) {
|
||||
if (bot.processNode.referenceParent.processDependencies.statusDependencies !== undefined) {
|
||||
thisObject.nodeArray = bot.processNode.referenceParent.processDependencies.statusDependencies
|
||||
} else {
|
||||
logger.write(MODULE_NAME, "[ERROR] initialize -> onInitilized -> It is not possible to not have status dependencies at all.");
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
logger.write(MODULE_NAME, "[ERROR] initialize -> onInitilized -> It is not possible to not have process dependencies, which means not status dependencies.");
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE)
|
||||
return
|
||||
}
|
||||
|
||||
/*For each dependency we will initialize the status report, and load it as part of this initialization process.*/
|
||||
|
||||
let alreadyCalledBack = false;
|
||||
let loadCount = 0;
|
||||
let dependenciesToProcess = []
|
||||
for (let i = 0; i < thisObject.nodeArray.length; i++) {
|
||||
let statusDependency = {
|
||||
dependency: thisObject.nodeArray[i]
|
||||
}
|
||||
|
||||
dependenciesToProcess.push(statusDependency)
|
||||
|
||||
}
|
||||
|
||||
for (let i = 0; i < dependenciesToProcess.length; i++) {
|
||||
|
||||
let statusReportModule = STATUS_REPORT.newStatusReport(BOT, logger, UTILITIES, PROCESS_OUTPUT);
|
||||
|
||||
logger.write(MODULE_NAME, "[INFO] initialize -> onInitilized -> Initializing Status Report # " + (i + 1));
|
||||
let statusDependency = dependenciesToProcess[i]
|
||||
statusReportModule.initialize(statusDependency.dependency, onInitilized);
|
||||
|
||||
function onInitilized(err) {
|
||||
|
||||
logger.write(MODULE_NAME, "[INFO] initialize -> onInitilized -> Initialized Status Report # " + (i + 1));
|
||||
|
||||
if (err.result !== global.DEFAULT_OK_RESPONSE.result) {
|
||||
if (alreadyCalledBack === false) {
|
||||
logger.write(MODULE_NAME, "[ERROR] initialize -> onInitilized -> err = " + err.stack);
|
||||
logger.write(MODULE_NAME, "[ERROR] initialize -> onInitilized -> err.message = " + err.message);
|
||||
alreadyCalledBack = true;
|
||||
callBackFunction(err);
|
||||
} else {
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[WARN] initialize -> Can not call back because I already did."); }
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
logger.write(MODULE_NAME, "[INFO] initialize -> onInitilized -> Loading Status Report # " + (i + 1));
|
||||
statusReportModule.load(onLoad);
|
||||
}
|
||||
|
||||
function onLoad(err) {
|
||||
|
||||
try {
|
||||
logger.write(MODULE_NAME, "[INFO] initialize -> onLoad -> Loaded Status Report # " + (i + 1));
|
||||
statusReportModule.status = err.message;
|
||||
|
||||
switch (err.message) {
|
||||
case global.DEFAULT_OK_RESPONSE.message: {
|
||||
addReport();
|
||||
return;
|
||||
}
|
||||
case "Status Report was never created.": {
|
||||
|
||||
logger.write(MODULE_NAME, "[WARN] initialize -> onLoad -> err = " + err.stack);
|
||||
logger.write(MODULE_NAME, "[WARN] initialize -> onLoad -> Report Not Found. -> Status Dependency = " + JSON.stringify(statusDependency.dependency))
|
||||
|
||||
addReport();
|
||||
return;
|
||||
}
|
||||
|
||||
case "Status Report is corrupt.": {
|
||||
|
||||
logger.write(MODULE_NAME, "[WARN] initialize -> onLoad -> err = " + err.stack);
|
||||
logger.write(MODULE_NAME, "[WARN] initialize -> onLoad -> Report Not Found. -> Status Dependency = " + JSON.stringify(statusDependency.dependency))
|
||||
|
||||
addReport();
|
||||
return;
|
||||
}
|
||||
default:
|
||||
{
|
||||
logger.write(MODULE_NAME, "[ERROR] initialize -> onLoad -> Operation Failed.");
|
||||
|
||||
if (alreadyCalledBack === false) {
|
||||
alreadyCalledBack = true;
|
||||
callBackFunction(err);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] initialize -> onLoad -> err = " + err.stack);
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
}
|
||||
}
|
||||
|
||||
function addReport() {
|
||||
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] initialize -> addReport -> Entering function."); }
|
||||
logger.write(MODULE_NAME, "[INFO] initialize -> addReport -> Adding Status Report # " + (i + 1));
|
||||
|
||||
loadCount++;
|
||||
logger.write(MODULE_NAME, "[INFO] initialize -> addReport -> Total Added = " + loadCount);
|
||||
|
||||
let key = statusDependency.dependency.dataMine + "-" + statusDependency.dependency.bot + "-" + statusDependency.dependency.process
|
||||
|
||||
thisObject.keys.push(key);
|
||||
thisObject.statusReports.set(key, statusReportModule);
|
||||
|
||||
if (statusReportModule.mainUtility !== undefined) {
|
||||
thisObject.reportsByMainUtility.set(statusReportModule.mainUtility, statusReportModule)
|
||||
}
|
||||
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] initialize -> addReport -> Report added to Map. -> key = " + key); }
|
||||
|
||||
if (loadCount === dependenciesToProcess.length) {
|
||||
if (alreadyCalledBack === false) {
|
||||
alreadyCalledBack = true
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE);
|
||||
return;
|
||||
} else {
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[WARN] initialize -> addReport -> Can not call back because I already did."); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] initialize -> err = " + err.stack);
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
}
|
||||
}
|
||||
|
||||
function finalize() {
|
||||
thisObject.statusReports.forEach(forEachStatusDependency)
|
||||
function forEachStatusDependency(statusDependency) {
|
||||
statusDependency.finalize()
|
||||
}
|
||||
thisObject.statusReports = undefined
|
||||
bot = undefined
|
||||
thisObject = undefined
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,417 @@
|
||||
exports.newStatusReport = function newStatusReport(BOT, logger, UTILITIES, PROCESS_OUTPUT) {
|
||||
|
||||
/*
|
||||
|
||||
A Status Report is a file that every bot reads at the begining of its execution and saves after it finishes its job.
|
||||
The purpose of the file is to record checkpoint information of what was the last thing done by the bot and helpfull enough to start the next execution.
|
||||
It usually does not include business related context data.
|
||||
|
||||
*/
|
||||
|
||||
const MODULE_NAME = "Status Report";
|
||||
|
||||
let bot = BOT;
|
||||
|
||||
let thisObject = {
|
||||
networkNode: undefined,
|
||||
mainUtility: undefined,
|
||||
file: undefined, // Here we have the JSON object representing the file content.
|
||||
initialize: initialize,
|
||||
finalize: finalize,
|
||||
load: load,
|
||||
save: save,
|
||||
asyncSave: asyncSave,
|
||||
status: undefined
|
||||
};
|
||||
|
||||
let statusDependencyNode;
|
||||
|
||||
/* Utilities needed. */
|
||||
|
||||
let utilities = UTILITIES.newCloudUtilities(logger);
|
||||
|
||||
/* Storage account to be used here. */
|
||||
|
||||
const FILE_STORAGE = require('./FileStorage.js');
|
||||
let fileStorage
|
||||
|
||||
let sessionPath = ''
|
||||
|
||||
return thisObject;
|
||||
|
||||
function initialize(pStatusDependencyNode, callBackFunction) {
|
||||
try {
|
||||
statusDependencyNode = pStatusDependencyNode;
|
||||
logger.fileName = MODULE_NAME + "." + statusDependencyNode.type + "." + statusDependencyNode.name + "." + statusDependencyNode.id;
|
||||
|
||||
/* Some very basic validations that we have all the information needed. */
|
||||
if (statusDependencyNode.referenceParent === undefined) {
|
||||
validationFailed(statusDependencyNode, "Status Dependency without Reference Parent.")
|
||||
return
|
||||
}
|
||||
|
||||
if (statusDependencyNode.referenceParent.parentNode === undefined) {
|
||||
validationFailed(statusDependencyNode.referenceParent, "Status Report not attached to a Process Definition.")
|
||||
return
|
||||
}
|
||||
|
||||
if (statusDependencyNode.referenceParent.parentNode.config.codeName === undefined) {
|
||||
validationFailed(statusDependencyNode.referenceParent.parentNode, "Process Definition witn no codeName defined.")
|
||||
return
|
||||
}
|
||||
|
||||
if (statusDependencyNode.referenceParent.parentNode.parentNode === undefined) {
|
||||
validationFailed(statusDependencyNode.referenceParent.parentNode, "Process Definition not attached to a Bot.")
|
||||
return
|
||||
}
|
||||
|
||||
if (statusDependencyNode.referenceParent.parentNode.parentNode.config.codeName === undefined) {
|
||||
validationFailed(statusDependencyNode.referenceParent.parentNode.parentNode, "Bot with no codeName defined.")
|
||||
return
|
||||
}
|
||||
|
||||
if (statusDependencyNode.referenceParent.parentNode.parentNode.parentNode === undefined) {
|
||||
validationFailed(statusDependencyNode.referenceParent.parentNode.parentNode, "Bot not attached to a Data Mine.")
|
||||
return
|
||||
}
|
||||
|
||||
if (statusDependencyNode.referenceParent.parentNode.parentNode.parentNode.config.codeName === undefined) {
|
||||
validationFailed(statusDependencyNode.referenceParent.parentNode.parentNode.parentNode, "Data Mine witn no codeName defined.")
|
||||
return
|
||||
}
|
||||
|
||||
function validationFailed(errorInNode, errorMessage) {
|
||||
let nodeString = JSON.stringify(errorInNode)
|
||||
logger.write(MODULE_NAME, "[ERROR] initialize -> " + errorMessage + ' -> nodeString = ' + nodeString)
|
||||
global.PROCESS_ERROR(bot.processKey, errorInNode, errorMessage)
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
}
|
||||
|
||||
/* Simplifying the access to basic info */
|
||||
statusDependencyNode.bot = statusDependencyNode.referenceParent.parentNode.parentNode.config.codeName
|
||||
statusDependencyNode.process = statusDependencyNode.referenceParent.parentNode.config.codeName
|
||||
statusDependencyNode.bottype = statusDependencyNode.referenceParent.parentNode.parentNode.type
|
||||
statusDependencyNode.dataMine = statusDependencyNode.referenceParent.parentNode.parentNode.parentNode.config.codeName
|
||||
statusDependencyNode.mineType = statusDependencyNode.referenceParent.parentNode.parentNode.parentNode.type.replace(' ', '-')
|
||||
statusDependencyNode.project = statusDependencyNode.referenceParent.parentNode.parentNode.parentNode.project
|
||||
|
||||
/* We retrieve the report main utility */
|
||||
if (statusDependencyNode.config !== undefined) {
|
||||
if (statusDependencyNode.config.mainUtility !== undefined) {
|
||||
thisObject.mainUtility = statusDependencyNode.config.mainUtility
|
||||
}
|
||||
}
|
||||
|
||||
if (bot.TRADING_SESSION !== undefined && statusDependencyNode.bottype === "Trading Bot") {
|
||||
sessionPath = bot.TRADING_SESSION.folderName + "/"
|
||||
}
|
||||
|
||||
if (bot.LEARNING_SESSION !== undefined && statusDependencyNode.bottype === "Learning Bot") {
|
||||
sessionPath = bot.LEARNING_SESSION.folderName + "/"
|
||||
}
|
||||
|
||||
/* Now we will see where do we need to fetch this status report from. */
|
||||
let network = global.TASK_NETWORK
|
||||
let processThisDependsOn = statusDependencyNode.referenceParent.parentNode
|
||||
|
||||
for (let i = 0; i < network.networkNodes.length; i++) {
|
||||
let networkNode = network.networkNodes[i]
|
||||
|
||||
if (checkThisDataBranch(networkNode.dataMining) === true) { return }
|
||||
if (checkThisTradingBranch(networkNode.testingEnvironment) === true) { return }
|
||||
if (checkThisTradingBranch(networkNode.productionEnvironment) === true) { return }
|
||||
|
||||
function checkThisDataBranch(branch) {
|
||||
if (branch === undefined) { return }
|
||||
for (let j = 0; j < branch.exchangeDataTasks.length; j++) {
|
||||
let exchangeTasks = branch.exchangeDataTasks[j]
|
||||
for (let p = 0; p < exchangeTasks.marketDataTasks.length; p++) {
|
||||
let marketTasks = exchangeTasks.marketDataTasks[p]
|
||||
if (marketTasks.referenceParent === undefined) { continue }
|
||||
if (marketTasks.referenceParent.id !== global.MARKET_NODE.id) { continue }
|
||||
for (let q = 0; q < marketTasks.dataMineTasks.length; q++) {
|
||||
let mineTasks = marketTasks.dataMineTasks[q]
|
||||
for (let k = 0; k < mineTasks.taskManagers.length; k++) {
|
||||
let taskManager = mineTasks.taskManagers[k]
|
||||
for (let m = 0; m < taskManager.tasks.length; m++) {
|
||||
let task = taskManager.tasks[m]
|
||||
if (task.bot === undefined) { continue }
|
||||
for (let n = 0; n < task.bot.processes.length; n++) {
|
||||
let process = task.bot.processes[n]
|
||||
if (process.referenceParent === undefined) { continue }
|
||||
let processDefinition = process.referenceParent
|
||||
if (processThisDependsOn.id === processDefinition.id) {
|
||||
if (process.type === 'Trading Process Instance') {
|
||||
if (process.session !== undefined) {
|
||||
if (bot.processNode.session.id !== process.session.id) {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
/*
|
||||
We found where the task that runs the process definition this status report depends on
|
||||
and where it is located on the network.
|
||||
*/
|
||||
thisObject.networkNode = networkNode
|
||||
if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { logger.write(MODULE_NAME, "[INFO] initialize -> Retrieving status report from " + networkNode.name + " -> host = " + networkNode.config.host + ' -> port = ' + networkNode.config.webPort + '.'); }
|
||||
|
||||
fileStorage = FILE_STORAGE.newFileStorage(logger, networkNode.config.host, networkNode.config.webPort);
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE);
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkThisTradingBranch(branch) {
|
||||
if (branch === undefined) { return }
|
||||
for (let j = 0; j < branch.exchangeTradingTasks.length; j++) {
|
||||
let exchangeTasks = branch.exchangeTradingTasks[j]
|
||||
for (let p = 0; p < exchangeTasks.marketTradingTasks.length; p++) {
|
||||
let marketTasks = exchangeTasks.marketTradingTasks[p]
|
||||
if (marketTasks.referenceParent === undefined) { continue }
|
||||
if (marketTasks.referenceParent.id !== global.MARKET_NODE.id) { continue }
|
||||
for (let q = 0; q < marketTasks.tradingMineTasks.length; q++) {
|
||||
let mineTasks = marketTasks.tradingMineTasks[q]
|
||||
for (let k = 0; k < mineTasks.taskManagers.length; k++) {
|
||||
let taskManager = mineTasks.taskManagers[k]
|
||||
for (let m = 0; m < taskManager.tasks.length; m++) {
|
||||
let task = taskManager.tasks[m]
|
||||
if (task.bot === undefined) { continue }
|
||||
for (let n = 0; n < task.bot.processes.length; n++) {
|
||||
let process = task.bot.processes[n]
|
||||
if (process.referenceParent === undefined) { continue }
|
||||
let processDefinition = process.referenceParent
|
||||
if (processThisDependsOn.id === processDefinition.id) {
|
||||
if (process.type === 'Trading Process Instance') {
|
||||
if (process.session !== undefined) {
|
||||
if (bot.processNode.session.id !== process.session.id) {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
/*
|
||||
We found where the task that runs the process definition this status report depends on
|
||||
and where it is located on the network.
|
||||
*/
|
||||
thisObject.networkNode = networkNode
|
||||
if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { logger.write(MODULE_NAME, "[INFO] initialize -> Retrieving status report from " + networkNode.name + " -> host = " + networkNode.config.host + ' -> port = ' + networkNode.config.webPort + '.'); }
|
||||
|
||||
fileStorage = FILE_STORAGE.newFileStorage(logger, networkNode.config.host, networkNode.config.webPort);
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE);
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.write(MODULE_NAME, "[ERROR] initialize -> Initialization Failed because we could not find where the data of this status report is located within the network. Check the logs for more info.");
|
||||
logger.write(MODULE_NAME, "[ERROR] initialize -> bot = " + statusDependencyNode.bot);
|
||||
logger.write(MODULE_NAME, "[ERROR] initialize -> process = " + statusDependencyNode.process);
|
||||
logger.write(MODULE_NAME, "[ERROR] initialize -> bottype = " + statusDependencyNode.bottype);
|
||||
logger.write(MODULE_NAME, "[ERROR] initialize -> dataMine = " + statusDependencyNode.dataMine);
|
||||
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] initialize -> err = " + err.stack);
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
}
|
||||
}
|
||||
|
||||
function finalize() {
|
||||
fileStorage = undefined
|
||||
thisObject.networkNode = undefined
|
||||
bot = undefined
|
||||
thisObject = undefined
|
||||
}
|
||||
|
||||
function load(callBackFunction) {
|
||||
|
||||
try {
|
||||
|
||||
if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { logger.write(MODULE_NAME, "[INFO] load -> Entering function."); }
|
||||
|
||||
let fileName = "Status.Report.json";
|
||||
let filePath;
|
||||
|
||||
let ownerId = statusDependencyNode.dataMine + "-" + statusDependencyNode.bot + "-" + statusDependencyNode.process
|
||||
let botId = bot.dataMine + "-" + bot.codeName + "-" + bot.process
|
||||
|
||||
if (ownerId !== botId) {
|
||||
|
||||
let filePathRoot = 'Project/' + statusDependencyNode.project + "/" + statusDependencyNode.mineType + "/" + statusDependencyNode.dataMine + "/" + statusDependencyNode.bot + '/' + bot.exchange + "/" + bot.market.baseAsset + "-" + bot.market.quotedAsset
|
||||
filePath = filePathRoot + "/Reports/" + sessionPath + statusDependencyNode.process;
|
||||
} else {
|
||||
filePath = bot.filePathRoot + "/Reports/" + sessionPath + statusDependencyNode.process;
|
||||
}
|
||||
|
||||
filePath += '/' + fileName
|
||||
|
||||
let canUserPrevious
|
||||
/*
|
||||
If we are funning Trading Engines, we can not allow ourselves to use a Status Report that is not the latest one, because it might contain
|
||||
transactioinal information related to the context of the operations the trading engine is doing.
|
||||
|
||||
On the contraty, if we are running a Sensor bot or an Indicator bot, we might, if necesary, use a previous version of a Status Report since
|
||||
there will be no big impact, just some reprocessing.
|
||||
*/
|
||||
if (bot.TRADING_SESSION !== undefined || bot.LEARNING_SESSION !== undefined) {
|
||||
canUserPrevious = false
|
||||
} else {
|
||||
canUserPrevious = true
|
||||
}
|
||||
|
||||
fileStorage.getTextFile(filePath, onFileReceived, undefined, canUserPrevious);
|
||||
|
||||
function onFileReceived(err, text) {
|
||||
|
||||
if (err.result === global.CUSTOM_FAIL_RESPONSE.result && (err.message === 'Folder does not exist.' || err.message === 'File does not exist.')
|
||||
|| err.code === "The specified key does not exist.") {
|
||||
|
||||
logger.write(MODULE_NAME, "[INFO] load -> onFileReceived -> err = " + err.message);
|
||||
|
||||
/* In this case we can assume that this is the first execution ever of this bot.*/
|
||||
|
||||
thisObject.file = JSON.parse('{}');
|
||||
|
||||
let customOK = {
|
||||
result: global.CUSTOM_OK_RESPONSE.result,
|
||||
message: "Status Report was never created."
|
||||
};
|
||||
logger.write(MODULE_NAME, "[WARN] load -> onFileReceived -> customOK = " + customOK.message);
|
||||
callBackFunction(customOK);
|
||||
return;
|
||||
}
|
||||
|
||||
if (err.result !== global.DEFAULT_OK_RESPONSE.result) {
|
||||
logger.write(MODULE_NAME, "[ERROR] load -> onFileReceived -> err = " + err.message);
|
||||
callBackFunction(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (global.LOG_CONTROL[MODULE_NAME].logContent === true) {
|
||||
logger.write(MODULE_NAME, "[INFO] load -> onFileReceived -> Content received = " + text);
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
thisObject.file = JSON.parse(text);
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE);
|
||||
|
||||
} catch (err) {
|
||||
|
||||
/*
|
||||
It might happen that the file content is corrupt. We will consider this as a temporary situation, since sometimes the file
|
||||
is being updated at the moment of the read. The bot can not run without a valid Status Report but we can request the platform to retry later.
|
||||
*/
|
||||
|
||||
logger.write(MODULE_NAME, "[ERROR] load -> onFileReceived -> Error Parsing the Status report. -> Err = " + err.message);
|
||||
logger.write(MODULE_NAME, "[WARN] load -> onFileReceived -> Error Parsing the Status report. -> text = " + text);
|
||||
|
||||
let customFail = {
|
||||
result: global.CUSTOM_FAIL_RESPONSE.result,
|
||||
message: "Status Report is corrupt."
|
||||
};
|
||||
logger.write(MODULE_NAME, "[WARN] load -> onFileReceived -> customFail = " + customFail.message);
|
||||
callBackFunction(customFail);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] load -> err = " + err.stack);
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
}
|
||||
}
|
||||
|
||||
function save(callBackFunction) {
|
||||
|
||||
try {
|
||||
|
||||
if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { logger.write(MODULE_NAME, "[INFO] save -> Entering function."); }
|
||||
|
||||
let ownerId = statusDependencyNode.dataMine + "-" + statusDependencyNode.bot + "-" + statusDependencyNode.process
|
||||
let botId = bot.dataMine + "-" + bot.codeName + "-" + bot.process
|
||||
|
||||
if (ownerId !== botId && statusDependencyNode.process !== "Context") { // Context is a special case where the report is created by the Context.js module itself.
|
||||
|
||||
let customErr = {
|
||||
result: global.CUSTOM_FAIL_RESPONSE.result,
|
||||
message: "Only bots owners of a Status Report can save them."
|
||||
};
|
||||
logger.write(MODULE_NAME, "[ERROR] save -> customErr = " + customErr.message);
|
||||
callBackFunction(customErr);
|
||||
return;
|
||||
}
|
||||
|
||||
let fileName = "Status.Report.json";
|
||||
let filePath = bot.filePathRoot + "/Reports/" + sessionPath + statusDependencyNode.process;
|
||||
|
||||
filePath += '/' + fileName
|
||||
let fileContent = JSON.stringify(thisObject.file);
|
||||
|
||||
fileStorage.createTextFile(filePath, fileContent + '\n', onFileCreated, true);
|
||||
|
||||
function onFileCreated(err) {
|
||||
|
||||
if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { logger.write(MODULE_NAME, "[INFO] save -> onFileCreated -> Entering function."); }
|
||||
|
||||
if (err.result !== global.DEFAULT_OK_RESPONSE.result) {
|
||||
logger.write(MODULE_NAME, "[ERROR] save -> onFileCreated -> err = " + err.stack);
|
||||
callBackFunction(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (global.LOG_CONTROL[MODULE_NAME].logContent === true) {
|
||||
logger.write(MODULE_NAME, "[INFO] save -> onFileCreated -> Content written = " + fileContent);
|
||||
}
|
||||
|
||||
/* All good, lets emit the event that means data has been updated. */
|
||||
|
||||
let processOutput = PROCESS_OUTPUT.newProcessOutput(bot, logger)
|
||||
|
||||
processOutput.raiseEvents(thisObject.file.lastFile, thisObject.file.timeFrames, callBackFunction);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] save -> err = " + err.stack);
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
}
|
||||
}
|
||||
|
||||
async function asyncSave() {
|
||||
|
||||
let fileName = "Status.Report.json";
|
||||
let filePath = bot.filePathRoot + "/Reports/" + sessionPath + statusDependencyNode.process;
|
||||
|
||||
filePath += '/' + fileName
|
||||
let fileContent = JSON.stringify(thisObject.file);
|
||||
|
||||
let response = await fileStorage.asyncCreateTextFile(filePath, fileContent + '\n', true)
|
||||
|
||||
if (response.err.result !== global.DEFAULT_OK_RESPONSE.result) {
|
||||
throw (response.err)
|
||||
}
|
||||
|
||||
/* All good, lets emit the event that means data has been updated. */
|
||||
let processOutput = PROCESS_OUTPUT.newProcessOutput(bot, logger)
|
||||
processOutput.asyncRaiseEvents(thisObject.file.lastFile, thisObject.file.timeFrames)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
exports.newTelegramBot = function newTelegramBot(bot, logger) {
|
||||
|
||||
const MODULE_NAME = 'Telegram Bot'
|
||||
|
||||
let thisObject = {
|
||||
telegramBot: undefined,
|
||||
telegramAPI: undefined,
|
||||
chatId: undefined,
|
||||
sendMessage: sendMessage,
|
||||
initialize: initialize,
|
||||
finalize: finalize
|
||||
}
|
||||
|
||||
return thisObject
|
||||
|
||||
function initialize(botToken, chatId) {
|
||||
/* Telegram Bot Initialization */
|
||||
try {
|
||||
thisObject.chatId = chatId
|
||||
|
||||
const Telegraf = require('telegraf')
|
||||
|
||||
thisObject.telegramBot = new Telegraf(botToken)
|
||||
thisObject.telegramBot.start((ctx) => ctx.reply('Hi! I am the Telegram Bot that will tell you what happens with your trading session.'))
|
||||
thisObject.telegramBot.help((ctx) => ctx.reply('I can stop the session if you type STOP.'))
|
||||
thisObject.telegramBot.hears('STOP', (ctx) => bot.TRADING_SESSION.stop(' by STOP command at Telegram.'))
|
||||
thisObject.telegramBot.launch()
|
||||
|
||||
const Telegram = require('telegraf/telegram')
|
||||
|
||||
thisObject.telegramAPI = new Telegram(botToken)
|
||||
|
||||
const message = "Telegram bot is starting."
|
||||
thisObject.telegramAPI.sendMessage(thisObject.chatId, message).catch(err => parentLogger.write(MODULE_NAME, "[WARN] initializeTelegramBot -> Telegram API error -> err = " + err))
|
||||
|
||||
} catch (err) {
|
||||
parentLogger.write(MODULE_NAME, "[WARN] initialize -> err = " + err.stack);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function finalize() {
|
||||
const message = "Telegram bot is signing off."
|
||||
thisObject.telegramAPI.sendMessage(thisObject.chatId, message).catch(err => parentLogger.write(MODULE_NAME, "[WARN] finalize -> Telegram API error -> err = " + err))
|
||||
}
|
||||
|
||||
function sendMessage(message) {
|
||||
thisObject.telegramAPI.sendMessage(thisObject.chatId, message).catch(err => parentLogger.write(MODULE_NAME, "[WARN] announce -> Telegram API error -> err = " + err))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,685 @@
|
||||
exports.newTradingBot = function newTradingBot(bot, parentLogger) {
|
||||
|
||||
const MODULE_NAME = "Trading Bot";
|
||||
const FULL_LOG = true;
|
||||
|
||||
const TRADING_PROCESS_MODULE = require(global.ROOT_DIR + '/LowFrequencyTrading/TradingProcess.js');
|
||||
const FILE_STORAGE = require('./FileStorage.js');
|
||||
const SESSION = require(global.ROOT_DIR + 'TradingSession');
|
||||
|
||||
let fileStorage = FILE_STORAGE.newFileStorage(parentLogger);
|
||||
let session = SESSION.newTradingSession(bot, parentLogger)
|
||||
|
||||
const DEBUG_MODULE = require(global.ROOT_DIR + 'DebugLog');
|
||||
let logger; // We need this here in order for the loopHealth function to work and be able to rescue the loop when it gets in trouble.
|
||||
|
||||
let nextLoopTimeoutHandle;
|
||||
|
||||
let thisObject = {
|
||||
initialize: initialize,
|
||||
run: run
|
||||
};
|
||||
|
||||
let processConfig;
|
||||
|
||||
return thisObject;
|
||||
|
||||
function initialize(pProcessConfig, callBackFunction) {
|
||||
/* This function is exactly the same in the 3 modules representing the 2 different bot types loops. */
|
||||
try {
|
||||
processConfig = pProcessConfig;
|
||||
if (bot.definedByUI === true) {
|
||||
/* The code of the bot is defined at the UI. No need to load a file with the code. */
|
||||
session.initialize(processConfig, callBackFunction)
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] initialize -> err = " + err.stack);
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
}
|
||||
}
|
||||
|
||||
function run(callBackFunction) {
|
||||
try {
|
||||
/* Some initial values*/
|
||||
let fixedTimeLoopIntervalHandle;
|
||||
bot.STOP_SESSION = true;
|
||||
if (FULL_LOG === true) { parentLogger.write(MODULE_NAME, '[IMPORTANT] run -> Stopping the Session now. ') }
|
||||
|
||||
/* Heartbeats sent to the UI */
|
||||
bot.processHeartBeat = processHeartBeat
|
||||
|
||||
loop();
|
||||
|
||||
function loop() {
|
||||
try {
|
||||
processHeartBeat(undefined, undefined, "Running...")
|
||||
function pad(str, max) {
|
||||
str = str.toString();
|
||||
return str.length < max ? pad(" " + str, max) : str;
|
||||
}
|
||||
|
||||
/* For each loop we want to create a new log file. */
|
||||
if (logger !== undefined) {
|
||||
logger.finalize()
|
||||
}
|
||||
logger = DEBUG_MODULE.newDebugLog();
|
||||
global.LOGGER_MAP.set(MODULE_NAME, logger)
|
||||
logger.bot = bot;
|
||||
logger.initialize();
|
||||
|
||||
bot.loopCounter++;
|
||||
bot.loopStartTime = new Date().valueOf();
|
||||
|
||||
let nextWaitTime;
|
||||
|
||||
/* We tell the UI that we are running. */
|
||||
processHeartBeat(undefined, undefined, "Running...")
|
||||
|
||||
/* We define here all the modules that the rest of the infraestructure, including the bots themselves can consume. */
|
||||
const UTILITIES = require(global.ROOT_DIR + 'CloudUtilities');
|
||||
const STATUS_REPORT = require(global.ROOT_DIR + 'StatusReport');
|
||||
const STATUS_DEPENDENCIES = require(global.ROOT_DIR + 'StatusDependencies');
|
||||
const DATA_DEPENDENCIES = require(global.ROOT_DIR + 'DataDependencies');
|
||||
const DATA_SET = require(global.ROOT_DIR + 'DataSet');
|
||||
const PROCESS_EXECUTION_EVENTS = require(global.ROOT_DIR + 'ProcessExecutionEvents');
|
||||
const PROCESS_OUTPUT = require(global.ROOT_DIR + 'ProcessOutput');
|
||||
|
||||
/* We define the datetime for the process that we are running now. This will be the official processing time for both the infraestructure and the bot. */
|
||||
bot.processDatetime = new Date(); // This will be considered the process date and time, so as to have it consistenly all over the execution.
|
||||
|
||||
/* High level log entry */
|
||||
console.log(new Date().toISOString() + " " + pad(bot.exchange, 20) + " " + pad(bot.market.baseAsset + '/' + bot.market.quotedAsset, 10) + " " + pad(bot.codeName, 30) + " " + pad(bot.process, 30)
|
||||
+ " Main Loop # " + pad(Number(bot.loopCounter), 8) + " " + bot.processNode.session.type + " " + bot.processNode.session.name)
|
||||
|
||||
/* Checking if we need to need to emit any event */
|
||||
if (bot.TRADING_SESSION_STATUS === 'Idle' && bot.STOP_SESSION === false) {
|
||||
bot.TRADING_SESSION_STATUS = 'Running'
|
||||
}
|
||||
|
||||
if (bot.TRADING_SESSION_STATUS === 'Running' && bot.STOP_SESSION === true) {
|
||||
bot.TRADING_SESSION_STATUS = 'Stopped'
|
||||
}
|
||||
|
||||
global.EMIT_SESSION_STATUS (bot.TRADING_SESSION_STATUS, bot.TRADING_SESSIONKey)
|
||||
|
||||
/* Checking if we should process this loop or not.*/
|
||||
if (bot.STOP_SESSION === true) {
|
||||
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] run -> loop -> Waiting for " + bot.processNode.session.type + " " + bot.processNode.session.name + " to be run."); }
|
||||
|
||||
console.log(new Date().toISOString() + " " + pad(bot.codeName, 20) + " " + pad(bot.process, 30)
|
||||
+ " Waiting for " + bot.processNode.session.type + " " + bot.processNode.session.name + " to be run. ");
|
||||
|
||||
nextWaitTime = 'Waiting for Session';
|
||||
loopControl(nextWaitTime);
|
||||
return
|
||||
}
|
||||
|
||||
/* We will prepare first the infraestructure needed for the bot to run. There are 3 modules we need to sucessfullly initialize first. */
|
||||
|
||||
let processExecutionEvents
|
||||
let userBot;
|
||||
let processFramework;
|
||||
let statusDependencies;
|
||||
let dataDependencies;
|
||||
|
||||
initializeProcessExecutionEvents();
|
||||
|
||||
function initializeProcessExecutionEvents() {
|
||||
try {
|
||||
processExecutionEvents = PROCESS_EXECUTION_EVENTS.newProcessExecutionEvents(bot, logger)
|
||||
processExecutionEvents.initialize(processConfig, onInizialized);
|
||||
|
||||
function onInizialized(err) {
|
||||
try {
|
||||
switch (err.result) {
|
||||
case global.DEFAULT_OK_RESPONSE.result: {
|
||||
logger.write(MODULE_NAME, "[INFO] run -> loop -> initializeProcessExecutionEvents -> onInizialized -> Execution finished well.");
|
||||
startProcessExecutionEvents()
|
||||
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, "[WARN] run -> loop -> initializeProcessExecutionEvents -> onInizialized -> Retry Later. Requesting Execution Retry.");
|
||||
nextWaitTime = 'Retry';
|
||||
loopControl(nextWaitTime);
|
||||
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] run -> loop -> initializeProcessExecutionEvents -> onInizialized -> Operation Failed. Aborting the process.");
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
default: {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeProcessExecutionEvents -> onInizialized -> Unhandled err.result received. -> err.result = " + err.result);
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeProcessExecutionEvents -> onInizialized -> Unhandled err.result received. -> err = " + err.message);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeProcessExecutionEvents -> onInizialized -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeProcessExecutionEvents -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function startProcessExecutionEvents() {
|
||||
try {
|
||||
processExecutionEvents.start(onStarted);
|
||||
|
||||
function onStarted(err) {
|
||||
try {
|
||||
switch (err.result) {
|
||||
case global.DEFAULT_OK_RESPONSE.result: {
|
||||
logger.write(MODULE_NAME, "[INFO] run -> loop -> startProcessExecutionEvents -> onStarted -> Execution finished well.");
|
||||
|
||||
if (global.STOP_TASK_GRACEFULLY === true) {
|
||||
loopControl()
|
||||
return
|
||||
}
|
||||
|
||||
initializeStatusDependencies();
|
||||
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, "[WARN] run -> loop -> startProcessExecutionEvents -> onStarted -> Retry Later. Requesting Execution Retry.");
|
||||
nextWaitTime = 'Retry';
|
||||
loopControl(nextWaitTime);
|
||||
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] run -> loop -> startProcessExecutionEvents -> onStarted -> Operation Failed. Aborting the process.");
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
default: {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> startProcessExecutionEvents -> onStarted -> Unhandled err.result received. -> err.result = " + err.result);
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> startProcessExecutionEvents -> onStarted -> Unhandled err.result received. -> err = " + err.message);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> startProcessExecutionEvents -> onStarted -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> startProcessExecutionEvents -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function initializeStatusDependencies() {
|
||||
try {
|
||||
statusDependencies = STATUS_DEPENDENCIES.newStatusDependencies(bot, logger, STATUS_REPORT, UTILITIES, PROCESS_OUTPUT);
|
||||
statusDependencies.initialize(onInizialized);
|
||||
|
||||
function onInizialized(err) {
|
||||
try {
|
||||
switch (err.result) {
|
||||
case global.DEFAULT_OK_RESPONSE.result: {
|
||||
logger.write(MODULE_NAME, "[INFO] run -> loop -> initializeStatusDependencies -> onInizialized -> Execution finished well.");
|
||||
initializeDataDependencies();
|
||||
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, "[WARN] run -> loop -> initializeStatusDependencies -> onInizialized -> Retry Later. Requesting Execution Retry.");
|
||||
nextWaitTime = 'Retry';
|
||||
loopControl(nextWaitTime);
|
||||
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] run -> loop -> initializeStatusDependencies -> onInizialized -> Operation Failed. Aborting the process.");
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
default: {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeStatusDependencies -> onInizialized -> Unhandled err.result received. -> err.result = " + err.result);
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeStatusDependencies -> onInizialized -> Unhandled err.result received. -> err = " + err.message);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeStatusDependencies -> onInizialized -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeStatusDependencies -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function initializeDataDependencies() {
|
||||
try {
|
||||
dataDependencies = DATA_DEPENDENCIES.newDataDependencies(bot, logger, DATA_SET);
|
||||
dataDependencies.initialize(onInizialized);
|
||||
|
||||
function onInizialized(err) {
|
||||
try {
|
||||
switch (err.result) {
|
||||
case global.DEFAULT_OK_RESPONSE.result: {
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] run -> loop -> initializeDataDependencies -> onInizialized -> Execution finished well."); }
|
||||
switch (processConfig.framework.name) {
|
||||
case 'Low-Frequency-Trading-Process': {
|
||||
processFramework = TRADING_PROCESS_MODULE.newTradingProcess(bot, logger, UTILITIES);
|
||||
intitializeProcessFramework();
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeDataDependencies -> onInizialized -> Process Framework not Supported.");
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeDataDependencies -> onInizialized -> Process Framework Name = " + processConfig.framework.name);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
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, "[WARN] run -> loop -> initializeDataDependencies -> onInizialized -> Retry Later. Requesting Execution Retry.");
|
||||
nextWaitTime = 'Retry';
|
||||
loopControl(nextWaitTime);
|
||||
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] run -> loop -> initializeDataDependencies -> onInizialized -> Operation Failed. Aborting the process.");
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
default: {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeDataDependencies -> onInizialized -> Unhandled err.result received. -> err.result = " + err.result);
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeDataDependencies -> onInizialized -> Unhandled err.result received. -> err = " + err.message);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeDataDependencies -> onInizialized -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> initializeDataDependencies -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function intitializeProcessFramework() {
|
||||
try {
|
||||
processFramework.initialize(processConfig, statusDependencies, dataDependencies, onInizialized);
|
||||
|
||||
function onInizialized(err) {
|
||||
try {
|
||||
switch (err.result) {
|
||||
case global.DEFAULT_OK_RESPONSE.result: {
|
||||
logger.write(MODULE_NAME, "[INFO] run -> loop -> intitializeProcessFramework -> onInizialized -> Execution finished well.");
|
||||
startProcessFramework();
|
||||
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, "[WARN] run -> loop -> intitializeProcessFramework -> onInizialized -> Retry Later. Requesting Execution Retry.");
|
||||
nextWaitTime = 'Retry';
|
||||
loopControl(nextWaitTime);
|
||||
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] run -> loop -> intitializeProcessFramework -> onInizialized -> Operation Failed. Aborting the process.");
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
case global.CUSTOM_OK_RESPONSE.result: {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> intitializeProcessFramework -> onInizialized > Unhandled custom response received. -> err = " + err.message);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
default: {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> intitializeProcessFramework -> onInizialized -> Unhandled err.result received. -> err.result = " + err.result);
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> intitializeProcessFramework -> onInizialized -> Unhandled err.result received. -> err = " + err.message);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> intitializeProcessFramework -> onInizialized -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> intitializeProcessFramework -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function startProcessFramework() {
|
||||
try {
|
||||
processFramework.start(onFinished);
|
||||
|
||||
function onFinished(err) {
|
||||
try {
|
||||
processFramework.finalize()
|
||||
processFramework = undefined
|
||||
dataDependencies.finalize()
|
||||
dataDependencies = undefined
|
||||
statusDependencies.finalize()
|
||||
statusDependencies = undefined
|
||||
|
||||
switch (err.result) {
|
||||
case global.DEFAULT_OK_RESPONSE.result: {
|
||||
logger.write(MODULE_NAME, "[INFO] run -> loop -> startProcessFramework -> onFinished -> Execution finished well.");
|
||||
nextWaitTime = 'Normal';
|
||||
finishProcessExecutionEvents()
|
||||
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, "[WARN] run -> loop -> startProcessFramework -> onFinished -> Retry Later. Requesting Execution Retry.");
|
||||
nextWaitTime = 'Retry';
|
||||
loopControl(nextWaitTime);
|
||||
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] run -> loop -> startProcessFramework -> onFinished -> Operation Failed. Aborting the process.");
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
case global.CUSTOM_OK_RESPONSE.result: {
|
||||
|
||||
switch (err.message) {
|
||||
case "Dependency does not exist.": {
|
||||
logger.write(MODULE_NAME, "[WARN] run -> loop -> startProcessFramework -> onFinished -> Dependency does not exist. This Loop will go to sleep.");
|
||||
nextWaitTime = 'Sleep';
|
||||
loopControl(nextWaitTime);
|
||||
return;
|
||||
}
|
||||
case "Dependency not ready.": {
|
||||
logger.write(MODULE_NAME, "[WARN] run -> loop -> startProcessFramework -> onFinished -> Dependency not ready. This Loop will go to sleep.");
|
||||
nextWaitTime = 'Sleep';
|
||||
loopControl(nextWaitTime);
|
||||
return;
|
||||
}
|
||||
default: {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> startProcessFramework -> onFinished -> Unhandled custom response received. -> err = " + err.message);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
default: {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> startProcessFramework -> onFinished -> Unhandled err.result received. -> err.result = " + err.result);
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> startProcessFramework -> onFinished -> Unhandled err.result received. -> err = " + err.message);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> startProcessFramework -> onFinished -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> startProcessFramework -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function finishProcessExecutionEvents() {
|
||||
try {
|
||||
processExecutionEvents.finish(onFinished);
|
||||
|
||||
function onFinished(err) {
|
||||
try {
|
||||
processExecutionEvents.finalize()
|
||||
processExecutionEvents = undefined
|
||||
|
||||
switch (err.result) {
|
||||
case global.DEFAULT_OK_RESPONSE.result: {
|
||||
logger.write(MODULE_NAME, "[INFO] run -> loop -> finishProcessExecutionEvents -> onFinished -> Execution finished well.");
|
||||
nextWaitTime = 'Normal';
|
||||
loopControl(nextWaitTime);
|
||||
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, "[WARN] run -> loop -> finishProcessExecutionEvents -> onFinished -> Retry Later. Requesting Execution Retry.");
|
||||
nextWaitTime = 'Retry';
|
||||
loopControl(nextWaitTime);
|
||||
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] run -> loop -> finishProcessExecutionEvents -> onFinished -> Operation Failed. Aborting the process.");
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
default: {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> finishProcessExecutionEvents -> onFinished -> Unhandled err.result received. -> err.result = " + err.result);
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> finishProcessExecutionEvents -> onFinished -> Unhandled err.result received. -> err = " + err.message);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> finishProcessExecutionEvents -> onFinished -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> finishProcessExecutionEvents -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function loopControl(nextWaitTime) {
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] run -> loop -> loopControl -> nextWaitTime = " + nextWaitTime); }
|
||||
|
||||
/* We show we reached the end of the loop. */
|
||||
processHeartBeat(undefined, undefined, "Running...")
|
||||
|
||||
/* Here we check if we must stop the loop gracefully. */
|
||||
shallWeStop(onStop, onContinue);
|
||||
|
||||
function onStop() {
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] run -> loop -> loopControl -> onStop -> Stopping the Loop Gracefully. See you next time!"); }
|
||||
|
||||
if (global.WRITE_LOGS_TO_FILES === 'true') {
|
||||
logger.persist();
|
||||
}
|
||||
|
||||
global.EVENT_SERVER_CLIENT.raiseEvent(bot.TRADING_SESSIONKey, 'Stopped')
|
||||
processStopped()
|
||||
return;
|
||||
}
|
||||
|
||||
function onContinue() {
|
||||
/* Indicator bots are going to be executed after a configured period of time after the last execution ended. This is to avoid overlapping executions. */
|
||||
switch (nextWaitTime) {
|
||||
case 'Waiting for Session': {
|
||||
let waitTime = processConfig.sessionRunWaitTime
|
||||
nextLoopTimeoutHandle = setTimeout(loop, waitTime);
|
||||
let waitingTime = waitTime / 1000 / 60
|
||||
let label = 'minute/s'
|
||||
if (waitingTime < 1) {
|
||||
waitingTime = waitTime / 1000
|
||||
label = 'seconds'
|
||||
}
|
||||
processHeartBeat(undefined, undefined, "Waiting " + waitingTime + " " + label + " for " + bot.processNode.session.type + " " + bot.processNode.session.name + " to be run. ")
|
||||
logger.persist();
|
||||
}
|
||||
break
|
||||
case 'Normal': {
|
||||
let waitTime
|
||||
if (processConfig.waitsForExecutionFinishedEvent === true) {
|
||||
waitTime = 0
|
||||
} else {
|
||||
switch (bot.TRADING_SESSION.type) {
|
||||
case 'Live Trading Session': {
|
||||
waitTime = bot.TRADING_SESSION.tradingParameters.timeFrame.config.value
|
||||
break
|
||||
}
|
||||
case 'Fordward Tessting Session': {
|
||||
waitTime = bot.TRADING_SESSION.tradingParameters.timeFrame.config.value
|
||||
break
|
||||
}
|
||||
case 'Paper Trading Session': {
|
||||
waitTime = bot.TRADING_SESSION.tradingParameters.timeFrame.config.value
|
||||
break
|
||||
}
|
||||
case 'Backtesting Session': {
|
||||
waitTime = 0
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] run -> loop -> loopControl -> Restarting Loop in " + (waitTime / 1000 / 60) + " minute/s."); }
|
||||
nextLoopTimeoutHandle = setTimeout(loop, waitTime);
|
||||
let waitingTime = waitTime / 1000 / 60
|
||||
let label = 'minute/s'
|
||||
if (waitingTime < 1) {
|
||||
waitingTime = waitTime / 1000
|
||||
label = 'seconds'
|
||||
}
|
||||
processHeartBeat(undefined, undefined, "Waiting " + waitingTime + " " + label + " for next execution.")
|
||||
logger.persist();
|
||||
}
|
||||
break;
|
||||
case 'Retry': {
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] run -> loop -> loopControl -> Restarting Loop in " + (processConfig.retryWaitTime / 1000) + " seconds."); }
|
||||
nextLoopTimeoutHandle = setTimeout(loop, processConfig.retryWaitTime);
|
||||
processHeartBeat(undefined, undefined, "Trying to recover from some problem. Waiting " + processConfig.retryWaitTime / 1000 + " seconds for next execution.")
|
||||
logger.persist();
|
||||
}
|
||||
break;
|
||||
case 'Sleep': {
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] run -> loop -> loopControl -> Restarting Loop in " + (processConfig.sleepWaitTime / 60000) + " minutes."); }
|
||||
nextLoopTimeoutHandle = setTimeout(loop, processConfig.sleepWaitTime);
|
||||
processHeartBeat(undefined, undefined, "Waiting " + processConfig.sleepWaitTime / 60000 + " minutes for next execution.")
|
||||
logger.persist();
|
||||
}
|
||||
break;
|
||||
case 'Coma': {
|
||||
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] run -> loop -> loopControl -> Restarting Loop in " + (processConfig.comaWaitTime / 3600000) + " hours."); }
|
||||
nextLoopTimeoutHandle = setTimeout(loop, processConfig.comaWaitTime);
|
||||
processHeartBeat(undefined, undefined, "Waiting " + processConfig.comaWaitTime / 3600000 + " hours for next execution.")
|
||||
logger.persist();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function shallWeStop(stopCallBack, continueCallBack) {
|
||||
try {
|
||||
/* IMPORTANT: This function is exactly the same on the 3 modules. */
|
||||
if (!global.STOP_TASK_GRACEFULLY) {
|
||||
continueCallBack();
|
||||
} else {
|
||||
stopCallBack();
|
||||
}
|
||||
} catch (err) {
|
||||
logger.write(MODULE_NAME, "[ERROR] run -> loop -> shallWeStop -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] run -> loop -> err = " + err.stack);
|
||||
global.unexpectedError = err.message
|
||||
processStopped()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function processHeartBeat(processingDate, percentage, status) {
|
||||
let event = {
|
||||
seconds: (new Date()).getSeconds(),
|
||||
processingDate: processingDate,
|
||||
percentage: percentage,
|
||||
status: status
|
||||
}
|
||||
global.EVENT_SERVER_CLIENT.raiseEvent(bot.processKey, 'Heartbeat', event)
|
||||
}
|
||||
|
||||
function processStopped() {
|
||||
if (global.unexpectedError !== undefined) {
|
||||
global.PROCESS_ERROR(bot.processKey, undefined, "An unexpected error caused the Process to stop.")
|
||||
} else {
|
||||
global.EVENT_SERVER_CLIENT.raiseEvent(bot.processKey, 'Stopped')
|
||||
}
|
||||
sessionStopped()
|
||||
logger.persist();
|
||||
clearInterval(fixedTimeLoopIntervalHandle);
|
||||
clearTimeout(nextLoopTimeoutHandle);
|
||||
if (global.unexpectedError !== undefined) {
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE)
|
||||
} else {
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE)
|
||||
}
|
||||
}
|
||||
|
||||
function sessionStopped() {
|
||||
if (bot.TRADING_SESSION_STATUS === 'Running') {
|
||||
global.EVENT_SERVER_CLIENT.raiseEvent(bot.TRADING_SESSIONKey, 'Stopped')
|
||||
bot.TRADING_SESSION_STATUS = 'Stopped'
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] run -> err = " + err.stack);
|
||||
clearInterval(fixedTimeLoopIntervalHandle);
|
||||
clearTimeout(nextLoopTimeoutHandle);
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,519 @@
|
||||
exports.newTradingSession = function newTradingSession(bot, parentLogger) {
|
||||
|
||||
const MODULE_NAME = "Trading Session"
|
||||
const FULL_LOG = true;
|
||||
|
||||
let thisObject = {
|
||||
initialize: initialize
|
||||
}
|
||||
|
||||
const SOCIAL_BOTS_MODULE = require('./SocialBots.js')
|
||||
let socialBotsModule = SOCIAL_BOTS_MODULE.newSocialBots(bot, parentLogger)
|
||||
|
||||
return thisObject;
|
||||
|
||||
function initialize(pProcessConfig, callBackFunction) {
|
||||
try {
|
||||
/* Initialize this info so that everything is logged propeerly */
|
||||
bot.TRADING_SESSION = {
|
||||
name: bot.processNode.session.name,
|
||||
id: bot.processNode.session.id
|
||||
}
|
||||
|
||||
/* Set the folderName for early logging */
|
||||
if (bot.processNode.session.config.folderName === undefined) {
|
||||
bot.TRADING_SESSION.folderName = bot.processNode.session.type.replace(' ', '-').replace(' ', '-') + '-' + bot.TRADING_SESSION.id
|
||||
} else {
|
||||
bot.TRADING_SESSION.folderName = bot.processNode.session.type.replace(' ', '-').replace(' ', '-') + '-' + bot.processNode.session.config.folderName
|
||||
}
|
||||
|
||||
/* Check if there is a session */
|
||||
if (bot.processNode.session === undefined) {
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] initialize -> Cannot run without a Session.");
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Listen to event to start or stop the session. */
|
||||
bot.TRADING_SESSIONKey = bot.processNode.session.name + '-' + bot.processNode.session.type + '-' + bot.processNode.session.id
|
||||
global.SESSION_MAP.set(bot.TRADING_SESSIONKey, bot.TRADING_SESSIONKey)
|
||||
|
||||
global.EVENT_SERVER_CLIENT.listenToEvent(bot.TRADING_SESSIONKey, 'Trading Session Status', undefined, bot.TRADING_SESSIONKey, undefined, onSessionStatus)
|
||||
global.EVENT_SERVER_CLIENT.listenToEvent(bot.TRADING_SESSIONKey, 'Run Trading Session', undefined, bot.TRADING_SESSIONKey, undefined, onSessionRun)
|
||||
global.EVENT_SERVER_CLIENT.listenToEvent(bot.TRADING_SESSIONKey, 'Stop Trading Session', undefined, bot.TRADING_SESSIONKey, undefined, onSessionStop)
|
||||
global.EVENT_SERVER_CLIENT.listenToEvent(bot.TRADING_SESSIONKey, 'Resume Trading Session', undefined, bot.TRADING_SESSIONKey, undefined, onSessionResume)
|
||||
|
||||
/* Connect this here so that it is accesible from other places */
|
||||
bot.TRADING_SESSIONError = sessionError
|
||||
bot.TRADING_SESSIONWarning = sessionWarning
|
||||
bot.TRADING_SESSIONInfo = sessionInfo
|
||||
|
||||
/* Heartbeats sent to the UI */
|
||||
bot.TRADING_SESSIONHeartBeat = sessionHeartBeat
|
||||
|
||||
callBackFunction(global.DEFAULT_OK_RESPONSE)
|
||||
return
|
||||
|
||||
function onSessionStatus() {
|
||||
if (bot.TRADING_SESSION_STATUS === 'Running') {
|
||||
let event = {
|
||||
status: 'Trading Session Runnning'
|
||||
}
|
||||
global.EVENT_SERVER_CLIENT.raiseEvent(bot.TRADING_SESSIONKey, 'Status Response', event)
|
||||
} else {
|
||||
let event = {
|
||||
status: 'Trading Session Not Runnning'
|
||||
}
|
||||
global.EVENT_SERVER_CLIENT.raiseEvent(bot.TRADING_SESSIONKey, 'Status Response', event)
|
||||
}
|
||||
}
|
||||
|
||||
function onSessionRun(message) {
|
||||
try {
|
||||
/* This happens when the UI is reloaded, the session was running and tries to run it again. */
|
||||
if (bot.TRADING_SESSION_STATUS === 'Idle' || bot.TRADING_SESSION_STATUS === 'Running') {
|
||||
parentLogger.write(MODULE_NAME, "[WARN] onSessionRun -> Event received to run the Session while it was already running. ")
|
||||
return
|
||||
}
|
||||
|
||||
/* We are going to run the Definition comming at the event. */
|
||||
bot.TRADING_SYSTEM = JSON.parse(message.event.tradingSystem)
|
||||
bot.TRADING_ENGINE = JSON.parse(message.event.tradingEngine)
|
||||
bot.TRADING_SESSION = JSON.parse(message.event.session)
|
||||
bot.DEPENDENCY_FILTER = JSON.parse(message.event.dependencyFilter)
|
||||
bot.RESUME = false
|
||||
bot.FIRST_EXECUTION = true
|
||||
bot.TRADING_SESSION.stop = stopSession // stop function
|
||||
|
||||
setUpSessionFolderName()
|
||||
|
||||
/* We validate all parameters received and complete some that might be missing if needed. */
|
||||
if (checkParemeters() === false) { return }
|
||||
|
||||
socialBotsModule.initialize()
|
||||
|
||||
let allGood
|
||||
switch (bot.TRADING_SESSION.type) {
|
||||
case 'Backtesting Session': {
|
||||
allGood = startBackTesting(message)
|
||||
break
|
||||
}
|
||||
case 'Live Trading Session': {
|
||||
allGood = startLiveTrading(message)
|
||||
break
|
||||
}
|
||||
case 'Fordward Testing Session': {
|
||||
allGood = startFordwardTesting(message)
|
||||
break
|
||||
}
|
||||
case 'Paper Trading Session': {
|
||||
allGood = startPaperTrading(message)
|
||||
break
|
||||
}
|
||||
}
|
||||
if (allGood === true) {
|
||||
bot.TRADING_SESSION_STATUS = 'Idle'
|
||||
bot.STOP_SESSION = false
|
||||
} else {
|
||||
bot.STOP_SESSION = true
|
||||
if (FULL_LOG === true) { parentLogger.write(MODULE_NAME, '[IMPORTANT] onSessionRun -> Stopping the Session now. ') }
|
||||
}
|
||||
|
||||
socialBotsModule.sendMessage(bot.TRADING_SESSION.type + " '" + bot.TRADING_SESSION.name + "' is starting.")
|
||||
} catch (err) {
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] initialize -> onSessionRun -> err = " + err.stack);
|
||||
}
|
||||
}
|
||||
|
||||
function onSessionStop() {
|
||||
stopSession('Session Stopped From the User Interface.')
|
||||
}
|
||||
|
||||
function onSessionResume(message) {
|
||||
try {
|
||||
if (bot.TRADING_SESSION.stop === undefined) {
|
||||
parentLogger.write(MODULE_NAME, "[WARN] onSessionResume -> Event received to resume the Session that have never be ran before. ")
|
||||
return
|
||||
}
|
||||
|
||||
/* This happens when the UI is reloaded, the session was running and tries to run it again. */
|
||||
if (bot.TRADING_SESSION_STATUS === 'Idle' || bot.TRADING_SESSION_STATUS === 'Running') {
|
||||
parentLogger.write(MODULE_NAME, "[WARN] onSessionResume -> Event received to resume the Session while it was already running. ")
|
||||
return
|
||||
}
|
||||
|
||||
bot.RESUME = true
|
||||
bot.STOP_SESSION = false
|
||||
|
||||
socialBotsModule.sendMessage(bot.TRADING_SESSION.type + " '" + bot.TRADING_SESSION.name + "' is resuming.")
|
||||
} catch (err) {
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] initialize -> onSessionResume -> err = " + err.stack);
|
||||
}
|
||||
}
|
||||
|
||||
function stopSession(commandOrigin) {
|
||||
|
||||
socialBotsModule.sendMessage(bot.TRADING_SESSION.type + " '" + bot.TRADING_SESSION.name + "' is stopping " + commandOrigin)
|
||||
socialBotsModule.finalize()
|
||||
bot.STOP_SESSION = true
|
||||
if (FULL_LOG === true) { parentLogger.write(MODULE_NAME, '[IMPORTANT] stopSession -> Stopping the Session now. ') }
|
||||
sessionInfo(bot.TRADING_SESSION, commandOrigin)
|
||||
}
|
||||
|
||||
function setUpSessionFolderName() {
|
||||
/*
|
||||
The session object is overwritten when the session is run. For that reason we
|
||||
need to setup again the folder name at the Session level.
|
||||
Set the folderName for logging, reports, context and data output
|
||||
*/
|
||||
let config
|
||||
if (bot.TRADING_SESSION.config !== undefined) {
|
||||
config = bot.TRADING_SESSION.config
|
||||
if (config.folderName === undefined) {
|
||||
bot.TRADING_SESSION.folderName = bot.processNode.session.type.replace(' ', '-').replace(' ', '-') + '-' + bot.TRADING_SESSION.id
|
||||
} else {
|
||||
bot.TRADING_SESSION.folderName = bot.processNode.session.type.replace(' ', '-').replace(' ', '-') + '-' + bot.processNode.session.config.folderName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkParemeters() {
|
||||
/*
|
||||
Here we check all the Session Parameters received. If something critical is missing we abort returning false. If something
|
||||
non critical is missing, we complete it with a default value.
|
||||
*/
|
||||
|
||||
if (bot.TRADING_SESSION.tradingParameters === undefined) {
|
||||
let errorMessage = "Session Node with no Parameters."
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] initialize -> checkParemeters -> " + errorMessage)
|
||||
bot.TRADING_SESSIONError(bot.TRADING_SESSION, errorMessage)
|
||||
return false
|
||||
}
|
||||
|
||||
/* Time Range */
|
||||
if (bot.TRADING_SESSION.tradingParameters.timeRange === undefined) { // if the Time Range is missing we create a default one.
|
||||
bot.TRADING_SESSION.tradingParameters.timeRange = {
|
||||
name: 'Missing Time Range',
|
||||
type: 'Time Range',
|
||||
config: {
|
||||
initialDatetime: (new Date()).valueOf(),
|
||||
finalDatetime: (new Date()).valueOf() + global.ONE_YEAR_IN_MILISECONDS
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/* Check that we received valid dates */
|
||||
if (bot.TRADING_SESSION.type === 'Backtesting Session') {
|
||||
if (isNaN(new Date(bot.TRADING_SESSION.tradingParameters.timeRange.config.initialDatetime)).valueOf()) {
|
||||
let errorMessage = "sessionParameters.timeRange.config.initialDatetime is not a valid date."
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] initialize -> checkParemeters -> " + errorMessage)
|
||||
bot.TRADING_SESSIONError(bot.TRADING_SESSION.tradingParameters, errorMessage)
|
||||
return false
|
||||
}
|
||||
}
|
||||
if (isNaN(new Date(bot.TRADING_SESSION.tradingParameters.timeRange.config.finalDatetime)).valueOf()) {
|
||||
let errorMessage = "sessionParameters.timeRange.config.initialDatetime is not a valid date."
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] initialize -> checkParemeters -> " + errorMessage)
|
||||
bot.TRADING_SESSIONError(bot.TRADING_SESSION.tradingParameters, errorMessage)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/* Session Type Forced Values */
|
||||
let today = (new Date()).valueOf()
|
||||
let aYearAgo = today - global.ONE_YEAR_IN_MILISECONDS
|
||||
let aYearFromNow = today + global.ONE_YEAR_IN_MILISECONDS
|
||||
switch (bot.TRADING_SESSION.type) {
|
||||
case 'Backtesting Session': {
|
||||
useDefaultDatetimes(aYearAgo, today)
|
||||
break
|
||||
}
|
||||
case 'Live Trading Session': {
|
||||
useDefaultDatetimes(today, aYearFromNow)
|
||||
break
|
||||
}
|
||||
case 'Fordward Testing Session': {
|
||||
useDefaultDatetimes(today, aYearFromNow)
|
||||
break
|
||||
}
|
||||
case 'Paper Trading Session': {
|
||||
useDefaultDatetimes(today, aYearFromNow)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function useDefaultDatetimes(initialDefault, finalDefault) {
|
||||
/*
|
||||
Note that inside the system, we are going to deal with these
|
||||
dates in their numeric value representation.
|
||||
*/
|
||||
|
||||
/* Initial Datetime */
|
||||
if (bot.TRADING_SESSION.type === 'Backtesting Session') {
|
||||
if (bot.TRADING_SESSION.tradingParameters.timeRange.config.initialDatetime === undefined) {
|
||||
bot.TRADING_SESSION.tradingParameters.timeRange.config.initialDatetime = initialDefault
|
||||
} else {
|
||||
bot.TRADING_SESSION.tradingParameters.timeRange.config.initialDatetime = (new Date(bot.TRADING_SESSION.tradingParameters.timeRange.config.initialDatetime)).valueOf()
|
||||
}
|
||||
} else {
|
||||
/* Non backtest session can start from the past only if explicitly configured that way */
|
||||
if (bot.TRADING_SESSION.tradingParameters.timeRange.config.initialDatetime === undefined || bot.TRADING_SESSION.tradingParameters.timeRange.config.allowStartingFromThePast !== true) {
|
||||
bot.TRADING_SESSION.tradingParameters.timeRange.config.initialDatetime = initialDefault
|
||||
} else {
|
||||
bot.TRADING_SESSION.tradingParameters.timeRange.config.initialDatetime = (new Date(bot.TRADING_SESSION.tradingParameters.timeRange.config.initialDatetime)).valueOf()
|
||||
}
|
||||
}
|
||||
|
||||
/* Final Datetime */
|
||||
if (bot.TRADING_SESSION.tradingParameters.timeRange.config.finalDatetime === undefined) {
|
||||
bot.TRADING_SESSION.tradingParameters.timeRange.config.finalDatetime = finalDefault
|
||||
} else {
|
||||
bot.TRADING_SESSION.tradingParameters.timeRange.config.finalDatetime = (new Date(bot.TRADING_SESSION.tradingParameters.timeRange.config.finalDatetime)).valueOf()
|
||||
}
|
||||
}
|
||||
|
||||
/* Time Frame */
|
||||
if (bot.TRADING_SESSION.tradingParameters.timeFrame === undefined) {
|
||||
let errorMessage = "Session Parameters Node with no Time Frame."
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] initialize -> checkParemeters -> " + errorMessage)
|
||||
bot.TRADING_SESSIONError(bot.TRADING_SESSION.tradingParameters, errorMessage)
|
||||
return false
|
||||
}
|
||||
if (bot.TRADING_SESSION.tradingParameters.timeFrame.config.label === undefined) {
|
||||
let errorMessage = "Session Parameters Node with no Time Frame Label configuration."
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] initialize -> checkParemeters -> " + errorMessage)
|
||||
bot.TRADING_SESSIONError(bot.TRADING_SESSION.tradingParameters.timeFrame, errorMessage)
|
||||
return false
|
||||
}
|
||||
bot.TRADING_SESSION.tradingParameters.timeFrame.config.value = getTimeFrameFromLabel(bot.TRADING_SESSION.tradingParameters.timeFrame.config.label)
|
||||
if (bot.TRADING_SESSION.tradingParameters.timeFrame.config.value === undefined) {
|
||||
let errorMessage = "Config error: label value not recognized. Try 01-min or 01-hs for example."
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] initialize -> checkParemeters -> " + errorMessage)
|
||||
bot.TRADING_SESSIONError(bot.TRADING_SESSION.tradingParameters.timeFrame, errorMessage)
|
||||
return false
|
||||
}
|
||||
|
||||
/* Session Base Asset */
|
||||
if (bot.TRADING_SESSION.tradingParameters.sessionBaseAsset === undefined) {
|
||||
let errorMessage = "Session Parameters Node with no Session Base Asset."
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] initialize -> checkParemeters -> " + errorMessage)
|
||||
bot.TRADING_SESSIONError(bot.TRADING_SESSION.tradingParameters, errorMessage)
|
||||
return false
|
||||
}
|
||||
if (bot.TRADING_SESSION.tradingParameters.sessionBaseAsset.config.initialBalance === undefined) {
|
||||
let errorMessage = "Session Parameters Session Base Asset with no initialBalance configuration."
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] initialize -> checkParemeters -> " + errorMessage)
|
||||
bot.TRADING_SESSIONError(bot.TRADING_SESSION.tradingParameters.sessionBaseAsset, errorMessage)
|
||||
return false
|
||||
}
|
||||
|
||||
/* Session Quoted Asset */
|
||||
if (bot.TRADING_SESSION.tradingParameters.sessionQuotedAsset === undefined) {
|
||||
let errorMessage = "Session Parameters Node with no Session Quoted Asset."
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] initialize -> checkParemeters -> " + errorMessage)
|
||||
bot.TRADING_SESSIONError(bot.TRADING_SESSION.tradingParameters, errorMessage)
|
||||
return false
|
||||
}
|
||||
if (bot.TRADING_SESSION.tradingParameters.sessionQuotedAsset.config.initialBalance === undefined) {
|
||||
let errorMessage = "Session Parameters Session Quoted Asset with no initialBalance configuration."
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] initialize -> checkParemeters -> " + errorMessage)
|
||||
bot.TRADING_SESSIONError(bot.TRADING_SESSION.tradingParameters.sessionQuotedAsset, errorMessage)
|
||||
return false
|
||||
}
|
||||
|
||||
/* Slippage */
|
||||
if (bot.TRADING_SESSION.tradingParameters.slippage === undefined) { // if the Slippage is missing we create a default one.
|
||||
bot.TRADING_SESSION.tradingParameters.slippage = {
|
||||
name: 'Missing Slippage',
|
||||
type: 'Slippage',
|
||||
config: {
|
||||
marketOrderRate: 0,
|
||||
stopLoss: 0,
|
||||
takeProfit: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
if (bot.TRADING_SESSION.tradingParameters.slippage.config.marketOrderRate === undefined) { // if the marketOrderRate is missing we create a default one.
|
||||
bot.TRADING_SESSION.tradingParameters.slippage.config.marketOrderRate = 0
|
||||
}
|
||||
if (bot.TRADING_SESSION.tradingParameters.slippage.config.stopLoss === undefined) { // if the stopLoss is missing we create a default one.
|
||||
bot.TRADING_SESSION.tradingParameters.slippage.config.stopLoss = 0
|
||||
}
|
||||
if (bot.TRADING_SESSION.tradingParameters.slippage.config.takeProfit === undefined) { // if the takeProfit is missing we create a default one.
|
||||
bot.TRADING_SESSION.tradingParameters.slippage.config.takeProfit = 0
|
||||
}
|
||||
|
||||
/* Fee Structure */
|
||||
if (bot.TRADING_SESSION.tradingParameters.feeStructure === undefined) { // if the Fee Structure is missing we create a default one.
|
||||
bot.TRADING_SESSION.tradingParameters.feeStructure = {
|
||||
name: 'Missing Fee Structure',
|
||||
type: 'Fee Structure',
|
||||
config: {
|
||||
maker: 0,
|
||||
taker: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
if (bot.TRADING_SESSION.tradingParameters.feeStructure.config.maker === undefined) { // if the maker is missing we create a default one.
|
||||
bot.TRADING_SESSION.tradingParameters.feeStructure.config.maker = 0
|
||||
}
|
||||
if (bot.TRADING_SESSION.tradingParameters.feeStructure.config.taker === undefined) { // if the taker is missing we create a default one.
|
||||
bot.TRADING_SESSION.tradingParameters.feeStructure.config.taker = 0
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function startBackTesting(message) {
|
||||
return true
|
||||
}
|
||||
|
||||
function startLiveTrading(message) {
|
||||
if (bot.KEY === undefined || bot.SECRET === undefined) {
|
||||
let errorMessage = "Key 'codeName' or 'secret' not provided. Plese check that and try again."
|
||||
if (FULL_LOG === true) { parentLogger.write(MODULE_NAME, "[ERROR] initialize -> startLiveTrading -> " + errorMessage); }
|
||||
bot.TRADING_SESSIONError(bot.TRADING_SESSION, errorMessage)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function startFordwardTesting(message) {
|
||||
if (bot.KEY === undefined || bot.SECRET === undefined) {
|
||||
if (FULL_LOG === true) { parentLogger.write(MODULE_NAME, "[WARN] initialize -> startFordwardTesting -> Key name or Secret not provided, not possible to run the process in Forward Testing mode."); }
|
||||
console.log("Key 'codeName' or 'secret' not provided. Plese check that and try again.")
|
||||
return false
|
||||
}
|
||||
|
||||
/* Reduce the balance */
|
||||
let balancePercentage = 1 // This is the default value
|
||||
|
||||
if (bot.TRADING_SESSION.config.balancePercentage !== undefined) {
|
||||
balancePercentage = bot.TRADING_SESSION.config.balancePercentage
|
||||
}
|
||||
|
||||
bot.TRADING_SESSION.tradingParameters.sessionBaseAsset.config.initialBalance = bot.TRADING_SESSION.tradingParameters.sessionBaseAsset.config.initialBalance * balancePercentage / 100
|
||||
bot.TRADING_SESSION.tradingParameters.sessionQuotedAsset.config.initialBalance = bot.TRADING_SESSION.tradingParameters.sessionQuotedAsset.config.initialBalance * balancePercentage / 100
|
||||
|
||||
bot.TRADING_SESSION.tradingParameters.sessionBaseAsset.config.minimumBalance = bot.TRADING_SESSION.tradingParameters.sessionBaseAsset.config.minimumBalance * balancePercentage / 100
|
||||
bot.TRADING_SESSION.tradingParameters.sessionQuotedAsset.config.minimumBalance = bot.TRADING_SESSION.tradingParameters.sessionQuotedAsset.config.minimumBalance * balancePercentage / 100
|
||||
|
||||
bot.TRADING_SESSION.tradingParameters.sessionBaseAsset.config.maximumBalance = bot.TRADING_SESSION.tradingParameters.sessionBaseAsset.config.maximumBalance * balancePercentage / 100
|
||||
bot.TRADING_SESSION.tradingParameters.sessionQuotedAsset.config.maximumBalance = bot.TRADING_SESSION.tradingParameters.sessionQuotedAsset.config.maximumBalance * balancePercentage / 100
|
||||
|
||||
pProcessConfig.normalWaitTime = bot.TRADING_SESSION.tradingParameters.timeFrame.config.value
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function startPaperTrading(message) {
|
||||
return true
|
||||
}
|
||||
|
||||
function getTimeFrameFromLabel(timeFrameLabel) {
|
||||
|
||||
for (let i = 0; i < global.marketFilesPeriods.length; i++) {
|
||||
let value = global.marketFilesPeriods[i][0]
|
||||
let label = global.marketFilesPeriods[i][1]
|
||||
|
||||
if (timeFrameLabel === label) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < global.dailyFilePeriods.length; i++) {
|
||||
let value = global.dailyFilePeriods[i][0]
|
||||
let label = global.dailyFilePeriods[i][1]
|
||||
|
||||
if (timeFrameLabel === label) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sessionHeartBeat(processingDate, percentage, status) {
|
||||
let event = {
|
||||
seconds: (new Date()).getSeconds(),
|
||||
processingDate: processingDate,
|
||||
percentage: percentage,
|
||||
status: status
|
||||
}
|
||||
global.EVENT_SERVER_CLIENT.raiseEvent(bot.TRADING_SESSIONKey, 'Heartbeat', event)
|
||||
|
||||
if (global.STOP_TASK_GRACEFULLY === true) {
|
||||
bot.STOP_SESSION = true
|
||||
if (FULL_LOG === true) { parentLogger.write(MODULE_NAME, '[IMPORTANT] sessionHeartBeat -> Stopping the Session now. ') }
|
||||
}
|
||||
}
|
||||
|
||||
function sessionError(node, errorMessage) {
|
||||
let event
|
||||
if (node !== undefined) {
|
||||
event = {
|
||||
nodeName: node.name,
|
||||
nodeType: node.type,
|
||||
nodeId: node.id,
|
||||
errorMessage: errorMessage
|
||||
}
|
||||
} else {
|
||||
event = {
|
||||
errorMessage: errorMessage
|
||||
}
|
||||
}
|
||||
global.EVENT_SERVER_CLIENT.raiseEvent(bot.TRADING_SESSIONKey, 'Error', event)
|
||||
|
||||
if (global.STOP_TASK_GRACEFULLY === true) {
|
||||
bot.STOP_SESSION = true
|
||||
if (FULL_LOG === true) { parentLogger.write(MODULE_NAME, '[IMPORTANT] sessionError -> Stopping the Session now. ') }
|
||||
}
|
||||
}
|
||||
|
||||
function sessionWarning(node, warningMessage) {
|
||||
let event
|
||||
if (node !== undefined) {
|
||||
event = {
|
||||
nodeName: node.name,
|
||||
nodeType: node.type,
|
||||
nodeId: node.id,
|
||||
warningMessage: warningMessage
|
||||
}
|
||||
} else {
|
||||
event = {
|
||||
warningMessage: warningMessage
|
||||
}
|
||||
}
|
||||
global.EVENT_SERVER_CLIENT.raiseEvent(bot.TRADING_SESSIONKey, 'Warning', event)
|
||||
|
||||
if (global.STOP_TASK_GRACEFULLY === true) {
|
||||
bot.STOP_SESSION = true
|
||||
if (FULL_LOG === true) { parentLogger.write(MODULE_NAME, '[IMPORTANT] sessionWarning -> Stopping the Session now. ') }
|
||||
}
|
||||
}
|
||||
|
||||
function sessionInfo(node, infoMessage) {
|
||||
let event
|
||||
if (node !== undefined) {
|
||||
event = {
|
||||
nodeName: node.name,
|
||||
nodeType: node.type,
|
||||
nodeId: node.id,
|
||||
infoMessage: infoMessage
|
||||
}
|
||||
} else {
|
||||
event = {
|
||||
infoMessage: infoMessage
|
||||
}
|
||||
}
|
||||
global.EVENT_SERVER_CLIENT.raiseEvent(bot.TRADING_SESSIONKey, 'Info', event)
|
||||
|
||||
if (global.STOP_TASK_GRACEFULLY === true) {
|
||||
bot.STOP_SESSION = true
|
||||
if (FULL_LOG === true) { parentLogger.write(MODULE_NAME, '[IMPORTANT] sessionInfo -> Stopping the Session now. ') }
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
parentLogger.write(MODULE_NAME, "[ERROR] initialize -> err = " + err.stack);
|
||||
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
|
||||
}
|
||||
}
|
||||
};
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
"use strict"
|
||||
|
||||
const { execSync } = require ('child_process')
|
||||
const log = require ('ololog').noLocate
|
||||
const { groupBy } = require ('../ccxt.js')
|
||||
const { values } = Object
|
||||
const assert = require ('assert')
|
||||
|
||||
function cleanupOldTags () {
|
||||
|
||||
const tags = execSync ('git tag').toString ().split ('\n').filter (s => s).map (t => {
|
||||
|
||||
const [major, minor, patch] = t.replace ('v', '').split ('.').map (Number)
|
||||
|
||||
assert (major < 100)
|
||||
assert (minor < 100)
|
||||
|
||||
return {
|
||||
key: (major * 100) + minor,
|
||||
tag: t,
|
||||
major,
|
||||
minor,
|
||||
patch,
|
||||
}
|
||||
})
|
||||
|
||||
const tagsByMajorMinor = values (groupBy (tags, 'key')).sort ((a, b) => a[0].key - b[0].key)
|
||||
|
||||
// Preserve all tags for first 5 minor versions
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
|
||||
const tags = tagsByMajorMinor.pop ()
|
||||
|
||||
if (tags) {
|
||||
log.green.bright ('Preserving', tags[0].tag, '...', tags[tags.length - 1].tag)
|
||||
}
|
||||
}
|
||||
|
||||
// For older versions, leave only "round" numbered versions (1/10th)
|
||||
|
||||
let tagsToDelete = []
|
||||
|
||||
for (const tags of tagsByMajorMinor) {
|
||||
|
||||
for (const { tag, patch } of tags) {
|
||||
|
||||
if (patch === 1) {
|
||||
log.green ('Preserving', tag)
|
||||
|
||||
} else {
|
||||
tagsToDelete.push (tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.bright.red ('Deleting', tagsToDelete.length, 'tags...')
|
||||
|
||||
if (!process.argv.includes ('--paper')) {
|
||||
|
||||
/* If it happens on a CI server, we don't want it to fail the build because of a super
|
||||
long execution time (one tag deletion takes ~5 sec...), hence that limit here */
|
||||
|
||||
if (process.argv.includes ('--limit')) {
|
||||
tagsToDelete = tagsToDelete.slice (-500)
|
||||
}
|
||||
|
||||
for (const tag of tagsToDelete) {
|
||||
|
||||
log.dim ('Deleting', tag)
|
||||
execSync (`git tag -d ${tag} && git push origin :refs/tags/${tag}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// main entry point
|
||||
|
||||
if (require.main === module) {
|
||||
|
||||
// if called directly like `node module`
|
||||
|
||||
cleanupOldTags ()
|
||||
|
||||
} else {
|
||||
|
||||
// do nothing if required as a module
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
module.exports = {
|
||||
cleanupOldTags,
|
||||
}
|
||||
+256
@@ -0,0 +1,256 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
|
||||
AF: 'Afghanistan',
|
||||
AX: 'Åland Islands',
|
||||
AL: 'Albania',
|
||||
DZ: 'Algeria',
|
||||
AS: 'American Samoa',
|
||||
AD: 'Andorra',
|
||||
AO: 'Angola',
|
||||
AI: 'Anguilla',
|
||||
AQ: 'Antarctica',
|
||||
AG: 'Antigua and Barbuda',
|
||||
AR: 'Argentina',
|
||||
AM: 'Armenia',
|
||||
AW: 'Aruba',
|
||||
AU: 'Australia',
|
||||
AT: 'Austria',
|
||||
AZ: 'Azerbaijan',
|
||||
BS: 'Bahamas',
|
||||
BH: 'Bahrain',
|
||||
BD: 'Bangladesh',
|
||||
BB: 'Barbados',
|
||||
BY: 'Belarus',
|
||||
BE: 'Belgium',
|
||||
BZ: 'Belize',
|
||||
BJ: 'Benin',
|
||||
BM: 'Bermuda',
|
||||
BT: 'Bhutan',
|
||||
BO: 'Bolivia',
|
||||
BQ: 'Bonaire, Sint Eustatius and Saba',
|
||||
BA: 'Bosnia and Herzegovina',
|
||||
BW: 'Botswana',
|
||||
BV: 'Bouvet Island',
|
||||
BR: 'Brazil',
|
||||
IO: 'British Indian Ocean Territory',
|
||||
BN: 'Brunei Darussalam',
|
||||
BG: 'Bulgaria',
|
||||
BF: 'Burkina Faso',
|
||||
BI: 'Burundi',
|
||||
KH: 'Cambodia',
|
||||
CM: 'Cameroon',
|
||||
CA: 'Canada',
|
||||
CV: 'Cabo Verde',
|
||||
KY: 'Cayman Islands',
|
||||
CF: 'Central African Republic',
|
||||
TD: 'Chad',
|
||||
CL: 'Chile',
|
||||
CN: 'China',
|
||||
CX: 'Christmas Island',
|
||||
CC: 'Cocos (Keeling) Islands',
|
||||
CO: 'Colombia',
|
||||
KM: 'Comoros',
|
||||
CG: 'Congo',
|
||||
CD: 'Congo',
|
||||
CK: 'Cook Islands',
|
||||
CR: 'Costa Rica',
|
||||
CI: 'Côte d\'Ivoire',
|
||||
HR: 'Croatia',
|
||||
CU: 'Cuba',
|
||||
CW: 'Curaçao',
|
||||
CY: 'Cyprus',
|
||||
CZ: 'Czech Republic',
|
||||
DK: 'Denmark',
|
||||
DJ: 'Djibouti',
|
||||
DM: 'Dominica',
|
||||
DO: 'Dominican Republic',
|
||||
EC: 'Ecuador',
|
||||
EG: 'Egypt',
|
||||
SV: 'El Salvador',
|
||||
GQ: 'Equatorial Guinea',
|
||||
ER: 'Eritrea',
|
||||
EE: 'Estonia',
|
||||
ET: 'Ethiopia',
|
||||
EU: 'EU',
|
||||
FK: 'Falkland Islands',
|
||||
FO: 'Faroe Islands',
|
||||
FJ: 'Fiji',
|
||||
FI: 'Finland',
|
||||
FR: 'France',
|
||||
GF: 'French Guiana',
|
||||
PF: 'French Polynesia',
|
||||
TF: 'French Southern Territories',
|
||||
GA: 'Gabon',
|
||||
GM: 'Gambia',
|
||||
GE: 'Georgia',
|
||||
DE: 'Germany',
|
||||
GH: 'Ghana',
|
||||
GI: 'Gibraltar',
|
||||
GR: 'Greece',
|
||||
GL: 'Greenland',
|
||||
GD: 'Grenada',
|
||||
GP: 'Guadeloupe',
|
||||
GU: 'Guam',
|
||||
GT: 'Guatemala',
|
||||
GG: 'Guernsey',
|
||||
GN: 'Guinea',
|
||||
GW: 'Guinea-Bissau',
|
||||
GY: 'Guyana',
|
||||
HT: 'Haiti',
|
||||
HM: 'Heard Island and McDonald Islands',
|
||||
VA: 'Holy See',
|
||||
HN: 'Honduras',
|
||||
HK: 'Hong Kong',
|
||||
HU: 'Hungary',
|
||||
IS: 'Iceland',
|
||||
IN: 'India',
|
||||
ID: 'Indonesia',
|
||||
IR: 'Iran',
|
||||
IQ: 'Iraq',
|
||||
IE: 'Ireland',
|
||||
IM: 'Isle of Man',
|
||||
IL: 'Israel',
|
||||
IT: 'Italy',
|
||||
JM: 'Jamaica',
|
||||
JP: 'Japan',
|
||||
JE: 'Jersey',
|
||||
JO: 'Jordan',
|
||||
KZ: 'Kazakhstan',
|
||||
KE: 'Kenya',
|
||||
KI: 'Kiribati',
|
||||
KP: 'North Korea (DPRK)',
|
||||
KR: 'South Korea',
|
||||
KW: 'Kuwait',
|
||||
KG: 'Kyrgyzstan',
|
||||
LA: 'Lao People\'s Democratic Republic',
|
||||
LV: 'Latvia',
|
||||
LB: 'Lebanon',
|
||||
LS: 'Lesotho',
|
||||
LR: 'Liberia',
|
||||
LY: 'Libya',
|
||||
LI: 'Liechtenstein',
|
||||
LT: 'Lithuania',
|
||||
LU: 'Luxembourg',
|
||||
MO: 'Macao',
|
||||
MK: 'Macedonia',
|
||||
MG: 'Madagascar',
|
||||
MW: 'Malawi',
|
||||
MY: 'Malaysia',
|
||||
MV: 'Maldives',
|
||||
ML: 'Mali',
|
||||
MT: 'Malta',
|
||||
MH: 'Marshall Islands',
|
||||
MQ: 'Martinique',
|
||||
MR: 'Mauritania',
|
||||
MU: 'Mauritius',
|
||||
YT: 'Mayotte',
|
||||
MX: 'Mexico',
|
||||
FM: 'Micronesia (Federated States of)',
|
||||
MD: 'Moldova',
|
||||
MC: 'Monaco',
|
||||
MN: 'Mongolia',
|
||||
ME: 'Montenegro',
|
||||
MS: 'Montserrat',
|
||||
MA: 'Morocco',
|
||||
MZ: 'Mozambique',
|
||||
MM: 'Myanmar',
|
||||
NA: 'Namibia',
|
||||
NR: 'Nauru',
|
||||
NP: 'Nepal',
|
||||
NL: 'Netherlands',
|
||||
NC: 'New Caledonia',
|
||||
NZ: 'New Zealand',
|
||||
NI: 'Nicaragua',
|
||||
NE: 'Niger',
|
||||
NG: 'Nigeria',
|
||||
NU: 'Niue',
|
||||
NF: 'Norfolk Island',
|
||||
MP: 'Northern Mariana Islands',
|
||||
NO: 'Norway',
|
||||
OM: 'Oman',
|
||||
PK: 'Pakistan',
|
||||
PW: 'Palau',
|
||||
PS: 'Palestine, State of',
|
||||
PA: 'Panama',
|
||||
PG: 'Papua New Guinea',
|
||||
PY: 'Paraguay',
|
||||
PE: 'Peru',
|
||||
PH: 'Philippines',
|
||||
PN: 'Pitcairn',
|
||||
PL: 'Poland',
|
||||
PT: 'Portugal',
|
||||
PR: 'Puerto Rico',
|
||||
QA: 'Qatar',
|
||||
RE: 'Réunion',
|
||||
RO: 'Romania',
|
||||
RU: 'Russia',
|
||||
RW: 'Rwanda',
|
||||
BL: 'Saint Barthélemy',
|
||||
SH: 'Saint Helena, Ascension and Tristan da Cunha',
|
||||
KN: 'Saint Kitts and Nevis',
|
||||
LC: 'Saint Lucia',
|
||||
MF: 'Saint Martin (French)',
|
||||
PM: 'Saint Pierre and Miquelon',
|
||||
VC: 'St. Vincent & Grenadines',
|
||||
WS: 'Samoa',
|
||||
SM: 'San Marino',
|
||||
ST: 'Sao Tome and Principe',
|
||||
SA: 'Saudi Arabia',
|
||||
SN: 'Senegal',
|
||||
RS: 'Serbia',
|
||||
SC: 'Seychelles',
|
||||
SL: 'Sierra Leone',
|
||||
SG: 'Singapore',
|
||||
SX: 'Sint Maarten (Dutch)',
|
||||
SK: 'Slovakia',
|
||||
SI: 'Slovenia',
|
||||
SB: 'Solomon Islands',
|
||||
SO: 'Somalia',
|
||||
ZA: 'South Africa',
|
||||
GS: 'South Georgia and the South Sandwich Islands',
|
||||
SS: 'South Sudan',
|
||||
ES: 'Spain',
|
||||
LK: 'Sri Lanka',
|
||||
SD: 'Sudan',
|
||||
SR: 'Suriname',
|
||||
SJ: 'Svalbard and Jan Mayen',
|
||||
SZ: 'Swaziland',
|
||||
SE: 'Sweden',
|
||||
CH: 'Switzerland',
|
||||
SY: 'Syrian Arab Republic',
|
||||
TW: 'Taiwan',
|
||||
TJ: 'Tajikistan',
|
||||
TZ: 'Tanzania',
|
||||
TH: 'Thailand',
|
||||
TL: 'Timor-Leste',
|
||||
TG: 'Togo',
|
||||
TK: 'Tokelau',
|
||||
TO: 'Tonga',
|
||||
TT: 'Trinidad and Tobago',
|
||||
TN: 'Tunisia',
|
||||
TR: 'Turkey',
|
||||
TM: 'Turkmenistan',
|
||||
TC: 'Turks and Caicos Islands',
|
||||
TV: 'Tuvalu',
|
||||
UG: 'Uganda',
|
||||
UA: 'Ukraine',
|
||||
AE: 'United Arab Emirates',
|
||||
GB: 'UK',
|
||||
UK: 'UK',
|
||||
US: 'US',
|
||||
UM: 'US Minor Islands',
|
||||
UY: 'Uruguay',
|
||||
UZ: 'Uzbekistan',
|
||||
VU: 'Vanuatu',
|
||||
VE: 'Venezuela',
|
||||
VN: 'Vietnam',
|
||||
VG: 'British Virgin Islands',
|
||||
VI: 'US Virgin Islands',
|
||||
WF: 'Wallis and Futuna',
|
||||
EH: 'Western Sahara',
|
||||
YE: 'Yemen',
|
||||
ZM: 'Zambia',
|
||||
ZW: 'Zimbabwe',
|
||||
}
|
||||
+455
@@ -0,0 +1,455 @@
|
||||
// ----------------------------------------------------------------------------
|
||||
// Usage:
|
||||
//
|
||||
// npm run export-exchanges
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
"use strict";
|
||||
|
||||
const fs = require ('fs')
|
||||
, countries = require ('./countries')
|
||||
, asTable = require ('as-table')
|
||||
, execSync = require ('child_process').execSync
|
||||
, log = require ('ololog').unlimited
|
||||
, ansi = require ('ansicolor').nice
|
||||
, { keys, values, entries } = Object
|
||||
, { replaceInFile } = require ('./fs.js')
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
function cloneGitHubWiki (gitWikiPath) {
|
||||
|
||||
if (!fs.existsSync (gitWikiPath)) {
|
||||
log.bright.cyan ('Cloning ccxt.wiki...')
|
||||
execSync ('git clone https://github.com/ccxt/ccxt.wiki.git ' + gitWikiPath)
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
function logExportExchanges (filename, regex, replacement) {
|
||||
log.bright.cyan ('Exporting exchanges →', filename.yellow)
|
||||
replaceInFile (filename, regex, replacement)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
function getIncludedExchangeIds () {
|
||||
|
||||
const includedIds = fs.readFileSync ('exchanges.cfg')
|
||||
.toString () // Buffer → String
|
||||
.split ('\n') // String → Array
|
||||
.map (line => line.split ('#')[0].trim ()) // trim comments
|
||||
.filter (exchange => exchange); // filter empty lines
|
||||
|
||||
const isIncluded = (id) => ((includedIds.length === 0) || includedIds.includes (id))
|
||||
|
||||
const ids = fs.readdirSync ('./js/')
|
||||
.filter (file => file.match (/[a-zA-Z0-9_-]+.js$/))
|
||||
.map (file => file.slice (0, -3))
|
||||
.filter (isIncluded);
|
||||
|
||||
return ids
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
function exportExchanges (replacements) {
|
||||
|
||||
log.bright.yellow ('Exporting exchanges...')
|
||||
|
||||
replacements.forEach (({ file, regex, replacement }) => {
|
||||
logExportExchanges (file, regex, replacement)
|
||||
})
|
||||
|
||||
log.bright.green ('Base sources updated successfully.')
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
function createExchanges (ids) {
|
||||
|
||||
const ccxt = require ('../ccxt.js')
|
||||
|
||||
const createExchange = (id) => {
|
||||
ccxt[id].prototype.checkRequiredDependencies = () => {} // suppress it
|
||||
return new (ccxt)[id] ()
|
||||
}
|
||||
|
||||
return ccxt.indexBy (ids.map (createExchange), 'id')
|
||||
}
|
||||
|
||||
// // ----------------------------------------------------------------------------
|
||||
// // strategically placed exactly here (we can require it AFTER the export)
|
||||
// const ccxt = require ('../ccxt.js')
|
||||
// // ----------------------------------------------------------------------------
|
||||
// // create exchanges
|
||||
// const createExchange = (id) => {
|
||||
// ccxt[id].prototype.checkRequiredDependencies = () => {} // suppress it
|
||||
// return new (ccxt)[id] ()
|
||||
// }
|
||||
// const exchanges = ccxt.indexBy (ids.map (createExchange), 'id')
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// TODO: REWRITE THIS ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
|
||||
|
||||
function exportSupportedAndCertifiedExchanges (exchanges, { allExchangesPaths, certifiedExchangesPaths, exchangesByCountriesPaths, proExchangesPaths }) {
|
||||
|
||||
// ............................................................................
|
||||
// markup constants and helper functions
|
||||
|
||||
const countryName = (code) => countries[code] || code
|
||||
|
||||
const ccxtCertifiedBadge = '[](https://github.com/ccxt/ccxt/wiki/Certification)'
|
||||
const ccxtProBadge = '[](https://ccxt.pro)'
|
||||
const logoHeading = ' '.repeat (7) + 'logo' + ' '.repeat (7)
|
||||
const tableHeadings = [ logoHeading, 'id', 'name', 'ver', 'doc', 'certified', 'pro' ]
|
||||
const exchangesByCountryHeading = [ 'country / region', ... tableHeadings.slice (0, 5) ]
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// list all supported exchanges
|
||||
|
||||
const exchangesNotListedInDocs = [ 'hitbtc2' ]
|
||||
|
||||
function makeTableData (exchanges) {
|
||||
return (
|
||||
values (exchanges)
|
||||
.filter (exchange => !exchangesNotListedInDocs.includes (exchange.id))
|
||||
.map (exchange => {
|
||||
let logo = exchange.urls['logo']
|
||||
let website = Array.isArray (exchange.urls.www) ? exchange.urls.www[0] : exchange.urls.www
|
||||
let url = exchange.urls.referral || website
|
||||
let doc = Array.isArray (exchange.urls.doc) ? exchange.urls.doc[0] : exchange.urls.doc
|
||||
let version = exchange.version ? exchange.version : '\*'
|
||||
let matches = version.match (/[^0-9]*([0-9].*)/)
|
||||
if (matches) {
|
||||
version = matches[1];
|
||||
}
|
||||
return [
|
||||
'[](' + url + ')',
|
||||
exchange.id,
|
||||
'[' + exchange.name + '](' + url + ')',
|
||||
version,
|
||||
'[API](' + doc + ')',
|
||||
exchange.certified ? ccxtCertifiedBadge : '',
|
||||
exchange.pro ? ccxtProBadge : '',
|
||||
]
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
function makeTable (jsonArray) {
|
||||
let table = asTable.configure ({ 'delimiter': ' | ' }) (jsonArray)
|
||||
let lines = table.split ("\n")
|
||||
lines.splice (1,0, lines[0].replace (/[^\|]/g, '-'))
|
||||
let headerLine = lines[1].split ('|')
|
||||
headerLine[3] = ':' + headerLine[3].slice (1, headerLine[3].length - 1) + ':'
|
||||
headerLine[4] = ':' + headerLine[4].slice (1, headerLine[4].length - 1) + ':'
|
||||
lines[1] = headerLine.join ('|')
|
||||
return lines.map (line => '|' + line + '|').join ("\n")
|
||||
}
|
||||
|
||||
if (allExchangesPaths) {
|
||||
|
||||
const tableData = makeTableData (exchanges)
|
||||
const numExchanges = tableData.length
|
||||
// prepend the table header
|
||||
tableData.splice (0, 0, tableHeadings)
|
||||
const exchangesTable = makeTable (tableData)
|
||||
const beginning = "The CCXT library currently supports the following "
|
||||
const ending = " cryptocurrency exchange markets and trading APIs:\n\n"
|
||||
const totalString = beginning + numExchanges + ending
|
||||
const allExchanges = totalString + exchangesTable + "$1"
|
||||
const allExchangesRegex = new RegExp ("[^\n]+[\n]{2}\\|[^`]+\\|([\n][\n]|[\n]$|$)", 'm')
|
||||
|
||||
for (const path of allExchangesPaths) {
|
||||
logExportExchanges (path, allExchangesRegex, allExchanges)
|
||||
}
|
||||
|
||||
// logExportExchanges ('README.md', allExchangesRegex, allExchanges)
|
||||
// logExportExchanges (wikiPath + '/Manual.md', allExchangesRegex, allExchanges)
|
||||
// logExportExchanges (wikiPath + '/Exchange-Markets.md', allExchangesRegex, allExchanges)
|
||||
}
|
||||
|
||||
if (proExchangesPaths) {
|
||||
const pro = values (exchanges).filter (exchange => exchange.pro)
|
||||
const tableData = makeTableData (pro)
|
||||
const numExchanges = tableData.length
|
||||
// prepend the table header
|
||||
tableData.splice (0, 0, tableHeadings)
|
||||
const exchangesTable = makeTable (tableData)
|
||||
const beginning = "The CCXT Pro library currently supports the following "
|
||||
const ending = " cryptocurrency exchange markets and WebSocket trading APIs:\n\n"
|
||||
const totalString = beginning + numExchanges + ending
|
||||
const proExchanges = totalString + exchangesTable + "$1"
|
||||
const proExchangesRegex = new RegExp ("[^\n]+[\n]{2}\\|[^`]+\\|([\n][\n]|[\n]$|$)", 'm')
|
||||
|
||||
for (const path of proExchangesPaths) {
|
||||
logExportExchanges (path, proExchangesRegex, proExchanges)
|
||||
}
|
||||
}
|
||||
|
||||
if (certifiedExchangesPaths) {
|
||||
const certified = values (exchanges).filter (exchange => exchange.certified)
|
||||
const tableData = makeTableData (certified)
|
||||
// prepend the table header
|
||||
tableData.splice (0, 0, tableHeadings)
|
||||
const certifiedExchangesRegex = new RegExp ("^(## Certified Cryptocurrency Exchanges\n{3})(?:\\|.+\\|$\n)+", 'm')
|
||||
const certifiedExchangesTable = makeTable (tableData)
|
||||
const certifiedExchanges = '$1' + certifiedExchangesTable + "\n"
|
||||
|
||||
for (const path of certifiedExchangesPaths) {
|
||||
logExportExchanges (path, certifiedExchangesRegex, certifiedExchanges)
|
||||
}
|
||||
|
||||
// logExportExchanges ('README.md', certifiedExchangesRegex, certifiedExchanges)
|
||||
}
|
||||
|
||||
if (exchangesByCountriesPaths) {
|
||||
let exchangesByCountries = []
|
||||
keys (countries).forEach (code => {
|
||||
let country = countries[code]
|
||||
let result = []
|
||||
keys (exchanges).forEach (id => {
|
||||
let exchange = exchanges[id]
|
||||
let logo = exchange.urls['logo']
|
||||
let website = Array.isArray (exchange.urls.www) ? exchange.urls.www[0] : exchange.urls.www
|
||||
let url = exchange.urls.referral || website
|
||||
let doc = Array.isArray (exchange.urls.doc) ? exchange.urls.doc[0] : exchange.urls.doc
|
||||
let version = exchange.version ? exchange.version : '\*'
|
||||
let matches = version.match (/[^0-9]*([0-9].*)/)
|
||||
if (matches)
|
||||
version = matches[1];
|
||||
let shouldInclude = false
|
||||
if (Array.isArray (exchange.countries)) {
|
||||
if (exchange.countries.indexOf (code) > -1)
|
||||
shouldInclude = true
|
||||
} else {
|
||||
if (code == exchange.countries)
|
||||
shouldInclude = true
|
||||
}
|
||||
if (shouldInclude) {
|
||||
let entry = [
|
||||
country,
|
||||
'[](' + url + ')',
|
||||
exchange.id,
|
||||
'[' + exchange.name + '](' + url + ')',
|
||||
version,
|
||||
'[API](' + doc + ')',
|
||||
// doesn't fit in width
|
||||
// exchange.certified ? ccxtCertifiedBadge : '',
|
||||
]
|
||||
result.push (entry)
|
||||
}
|
||||
})
|
||||
exchangesByCountries = exchangesByCountries.concat (result)
|
||||
});
|
||||
|
||||
const countryKeyIndex = exchangesByCountryHeading.indexOf ('country / region')
|
||||
exchangesByCountries = exchangesByCountries.sort ((a, b) => {
|
||||
const countryA = a[countryKeyIndex].toLowerCase ()
|
||||
const countryB = b[countryKeyIndex].toLowerCase ()
|
||||
if (countryA > countryB) {
|
||||
return 1
|
||||
} else if (countryA < countryB) {
|
||||
return -1;
|
||||
} else {
|
||||
if (a['id'] > b['id'])
|
||||
return 1;
|
||||
else if (a['id'] < b['id'])
|
||||
return -1;
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
})
|
||||
|
||||
exchangesByCountries.splice (0, 0, exchangesByCountryHeading)
|
||||
const lines = makeTable (exchangesByCountries)
|
||||
const result = "# Exchanges By Country\n\nThe ccxt library currently supports the following cryptocurrency exchange markets and trading APIs:\n\n" + lines + "\n\n"
|
||||
for (const path of exchangesByCountriesPaths) {
|
||||
fs.truncateSync (path)
|
||||
fs.writeFileSync (path, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: REWRITE THIS ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
function exportExchangeIdsToExchangesJson (exchanges) {
|
||||
log.bright ('Exporting exchange ids to'.cyan, 'exchanges.json'.yellow)
|
||||
const ids = keys (exchanges)
|
||||
console.log (ids)
|
||||
fs.writeFileSync ('exchanges.json', JSON.stringify ({ ids }, null, 4))
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
function exportWikiToGitHub (wikiPath, gitWikiPath) {
|
||||
|
||||
log.bright.cyan ('Exporting wiki to GitHub')
|
||||
|
||||
const ccxtWikiFiles = {
|
||||
'README.md': 'Home.md',
|
||||
'Install.md': 'Install.md',
|
||||
'Manual.md': 'Manual.md',
|
||||
'Exchange-Markets.md': 'Exchange-Markets.md',
|
||||
'Exchange-Markets-By-Country.md': 'Exchange-Markets-By-Country.md',
|
||||
'ccxt.pro.md': 'ccxt.pro.md',
|
||||
'ccxt.pro.install.md': 'ccxt.pro.install.md',
|
||||
'ccxt.pro.manual.md': 'ccxt.pro.manual.md',
|
||||
}
|
||||
|
||||
for (const [ sourceFile, destinationFile ] of entries (ccxtWikiFiles)) {
|
||||
|
||||
const sourcePath = wikiPath + '/' + sourceFile
|
||||
const destinationPath = gitWikiPath + '/' + destinationFile
|
||||
log.bright.cyan ('Exporting', sourcePath.yellow, '→', destinationPath.yellow)
|
||||
fs.writeFileSync (destinationPath, fs.readFileSync (sourcePath))
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
function exportKeywordsToPackageJson (exchanges) {
|
||||
|
||||
log.bright ('Exporting exchange keywords to'.cyan, 'package.json'.yellow)
|
||||
|
||||
// const packageJSON = require ('../package.json')
|
||||
const packageJSON = JSON.parse (fs.readFileSync ('./package.json'))
|
||||
const keywords = new Set (packageJSON.keywords)
|
||||
|
||||
for (const ex of values (exchanges)) {
|
||||
for (const url of Array.isArray (ex.urls.www) ? ex.urls.www : [ex.urls.www]) {
|
||||
keywords.add (url.replace (/(http|https):\/\/(www\.)?/, '').replace (/\/.*/, ''))
|
||||
}
|
||||
keywords.add (ex.name)
|
||||
}
|
||||
|
||||
packageJSON.keywords = [...keywords]
|
||||
fs.writeFileSync ('./package.json', JSON.stringify (packageJSON, null, 2) + "\n")
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
function flatten (nested, result = []) {
|
||||
for (const key in nested) {
|
||||
result.push (key)
|
||||
if (Object.keys (nested[key]).length)
|
||||
flatten (nested[key], result)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
function exportEverything () {
|
||||
const ids = getIncludedExchangeIds ()
|
||||
const errorHierarchy = require ('../js/base/errorHierarchy.js')
|
||||
const flat = flatten (errorHierarchy)
|
||||
flat.push ('error_hierarchy')
|
||||
|
||||
const replacements = [
|
||||
{
|
||||
file: './ccxt.js',
|
||||
regex: /(?:const|var)\s+exchanges\s+\=\s+\{[^\}]+\}/,
|
||||
replacement: "const exchanges = {\n" + ids.map (id => (" '" + id + "':").padEnd (30) + " require ('./js/" + id + ".js'),").join ("\n") + " \n}",
|
||||
},
|
||||
{
|
||||
file: './python/ccxt/__init__.py',
|
||||
regex: /exchanges \= \[[^\]]+\]/,
|
||||
replacement: "exchanges = [\n" + " '" + ids.join ("',\n '") + "'," + "\n]",
|
||||
},
|
||||
{
|
||||
file: './python/ccxt/__init__.py',
|
||||
regex: /(?:from ccxt\.[^\.]+ import [^\s]+\s+\# noqa\: F401[\r]?[\n])+[\r]?[\n]exchanges/,
|
||||
replacement: ids.map (id => ('from ccxt.' + id + ' import ' + id).padEnd (60) + '# noqa: F401').join ("\n") + "\n\nexchanges",
|
||||
},
|
||||
{
|
||||
file: './python/ccxt/__init__.py',
|
||||
regex: /(?:from ccxt\.base\.errors import [^\s]+\s+\# noqa\: F401[\r]?[\n])+[\r]?[\n]/,
|
||||
replacement: flat.map (error => ('from ccxt.base.errors' + ' import ' + error).padEnd (60) + '# noqa: F401').join ("\n") + "\n\n",
|
||||
},
|
||||
{
|
||||
file: './python/ccxt/async_support/__init__.py',
|
||||
regex: /(?:from ccxt\.base\.errors import [^\s]+\s+\# noqa\: F401[\r]?[\n])+[\r]?[\n]/,
|
||||
replacement: flat.map (error => ('from ccxt.base.errors' + ' import ' + error).padEnd (60) + '# noqa: F401').join ("\n") + "\n\n",
|
||||
},
|
||||
{
|
||||
file: './python/ccxt/async_support/__init__.py',
|
||||
regex: /(?:from ccxt\.async_support\.[^\.]+ import [^\s]+\s+\# noqa\: F401[\r]?[\n])+[\r]?[\n]exchanges/,
|
||||
replacement: ids.map (id => ('from ccxt.async_support.' + id + ' import ' + id).padEnd (74) + '# noqa: F401').join ("\n") + "\n\nexchanges",
|
||||
},
|
||||
{
|
||||
file: './python/ccxt/async_support/__init__.py',
|
||||
regex: /exchanges \= \[[^\]]+\]/,
|
||||
replacement: "exchanges = [\n" + " '" + ids.join ("',\n '") + "'," + "\n]",
|
||||
},
|
||||
{
|
||||
file: './php/base/Exchange.php',
|
||||
regex: /public static \$exchanges \= array\s*\([^\)]+\)/,
|
||||
replacement: "public static $exchanges = array(\n '" + ids.join ("',\n '") + "',\n )",
|
||||
},
|
||||
]
|
||||
|
||||
exportExchanges (replacements)
|
||||
|
||||
// strategically placed exactly here (we can require it AFTER the export)
|
||||
const exchanges = createExchanges (ids)
|
||||
|
||||
const wikiPath = 'wiki'
|
||||
, gitWikiPath = 'build/ccxt.wiki'
|
||||
|
||||
cloneGitHubWiki (gitWikiPath)
|
||||
|
||||
exportSupportedAndCertifiedExchanges (exchanges, {
|
||||
allExchangesPaths: [
|
||||
'README.md',
|
||||
wikiPath + '/Manual.md',
|
||||
wikiPath + '/Exchange-Markets.md'
|
||||
],
|
||||
certifiedExchangesPaths: [
|
||||
'README.md',
|
||||
],
|
||||
exchangesByCountriesPaths: [
|
||||
wikiPath + '/Exchange-Markets-By-Country.md'
|
||||
],
|
||||
proExchangesPaths: [
|
||||
wikiPath + '/ccxt.pro.manual.md',
|
||||
],
|
||||
})
|
||||
|
||||
exportExchangeIdsToExchangesJson (exchanges)
|
||||
exportWikiToGitHub (wikiPath, gitWikiPath)
|
||||
exportKeywordsToPackageJson (exchanges)
|
||||
|
||||
log.bright.green ('Exported successfully.')
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// main entry point
|
||||
|
||||
if (require.main === module) {
|
||||
|
||||
// if called directly like `node module`
|
||||
|
||||
exportEverything ()
|
||||
|
||||
} else {
|
||||
|
||||
// do nothing if required as a module
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
module.exports = {
|
||||
cloneGitHubWiki,
|
||||
getIncludedExchangeIds,
|
||||
exportExchanges,
|
||||
createExchanges,
|
||||
exportSupportedAndCertifiedExchanges,
|
||||
exportExchangeIdsToExchangesJson,
|
||||
exportWikiToGitHub,
|
||||
exportKeywordsToPackageJson,
|
||||
exportEverything,
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
"use strict";
|
||||
|
||||
const fs = require ('fs')
|
||||
, path = require ('path')
|
||||
|
||||
function replaceInFile (filename, regex, replacement) {
|
||||
const contents = fs.readFileSync (filename, 'utf8')
|
||||
const newContents = contents.replace (regex, replacement)
|
||||
fs.truncateSync (filename)
|
||||
fs.writeFileSync (filename, newContents)
|
||||
}
|
||||
|
||||
function copyFile (oldName, newName) {
|
||||
const contents = fs.readFileSync (oldName, 'utf8')
|
||||
fs.truncateSync (newName)
|
||||
fs.writeFileSync (newName, contents)
|
||||
}
|
||||
|
||||
function overwriteFile (filename, contents) {
|
||||
// log.cyan ('Overwriting → ' + filename.yellow)
|
||||
fs.closeSync (fs.openSync (filename, 'a'));
|
||||
fs.truncateSync (filename)
|
||||
fs.writeFileSync (filename, contents)
|
||||
}
|
||||
|
||||
function createFolder (folder) {
|
||||
try {
|
||||
fs.mkdirSync (folder)
|
||||
} catch (err) {
|
||||
if (err.code !== 'EEXIST') {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createFolderRecursively (folder) {
|
||||
const parts = folder.split (path.sep)
|
||||
for (let i = 1; i <= parts.length; i++) {
|
||||
createFolder (path.join.apply (null, parts.slice (0, i)))
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
replaceInFile,
|
||||
copyFile,
|
||||
overwriteFile,
|
||||
createFolder,
|
||||
createFolderRecursively,
|
||||
}
|
||||
+1350
File diff suppressed because it is too large.
Load diff
+87
@@ -0,0 +1,87 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// Usage:
|
||||
//
|
||||
// npm run vss
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
"use strict";
|
||||
|
||||
const fs = require ('fs')
|
||||
const log = require ('ololog')
|
||||
const ansi = require ('ansicolor').nice
|
||||
const { execSync } = require ('child_process')
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
function incrementVersionPatchNumber (version) {
|
||||
|
||||
let [ major, minor, patch ] = version.split ('.')
|
||||
|
||||
// we don't increment it here anymore, because
|
||||
// npm version patch will be explicitly called before
|
||||
|
||||
// patch = (parseInt (patch) + 1).toString ()
|
||||
|
||||
version = [ major, minor, patch ].join ('.')
|
||||
|
||||
return version
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
function vss (filename, template, version) {
|
||||
log.bright.cyan ('Single-sourcing version', version, './package.json → ' + filename.yellow)
|
||||
const content = fs.readFileSync (filename, 'utf8')
|
||||
const regexp = new RegExp (template.replace (/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') // escape string for use in regexp
|
||||
.replace ('\\{version\\}', '\\d+\\.\\d+\\.\\d+'), 'g')
|
||||
fs.truncateSync (filename)
|
||||
fs.writeFileSync (filename, content.replace (regexp, template.replace ('{version}', version)))
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
function vssEverything () {
|
||||
|
||||
let { version } = require ('../package.json')
|
||||
|
||||
log.bright ('Old version: '.dim, version)
|
||||
version = incrementVersionPatchNumber (version)
|
||||
log.bright ('New version: '.cyan, version)
|
||||
|
||||
vss ('./ccxt.js', "const version = '{version}'", version)
|
||||
vss ('./php/base/Exchange.php', "$version = '{version}'", version)
|
||||
vss ('./php/base/Exchange.php', "VERSION = '{version}'", version)
|
||||
vss ('./python/ccxt/__init__.py', "__version__ = '{version}'", version)
|
||||
vss ('./python/ccxt/base/exchange.py', "__version__ = '{version}'", version)
|
||||
vss ('./python/ccxt/async_support/__init__.py', "__version__ = '{version}'", version)
|
||||
vss ('./python/ccxt/async_support/base/exchange.py', "__version__ = '{version}'", version)
|
||||
|
||||
vss ('./README.md', "ccxt@{version}", version)
|
||||
vss ('./wiki/Install.md', "ccxt@{version}", version)
|
||||
|
||||
execSync ('cp ./package.json ./LICENSE.txt ./keys.json ./python/')
|
||||
|
||||
log.bright.green ('Version single-sourced successfully.')
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// main entry point
|
||||
|
||||
if (require.main === module) {
|
||||
|
||||
// if called directly like `node module`
|
||||
|
||||
vssEverything ()
|
||||
|
||||
} else {
|
||||
|
||||
// do nothing if required as a module
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
module.exports = {
|
||||
incrementVersionPatchNumber,
|
||||
vss,
|
||||
vssEverything,
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
/* A entry point for the browser bundle version. This gets compiled by:
|
||||
|
||||
browserify --debug ./ccxt.browser.js > ./dist/ccxt.browser.js
|
||||
*/
|
||||
|
||||
window.ccxt = require ('./ccxt')
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
"use strict";
|
||||
|
||||
/*
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 Igor Kroitor
|
||||
|
||||
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 without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
const Exchange = require ('./js/base/Exchange')
|
||||
, functions = require ('./js/base/functions')
|
||||
, errors = require ('./js/base/errors')
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// this is updated by vss.js when building
|
||||
|
||||
const version = '1.27.45'
|
||||
|
||||
Exchange.ccxtVersion = version
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
const exchanges = {
|
||||
'_1btcxe': require ('./js/_1btcxe.js'),
|
||||
'acx': require ('./js/acx.js'),
|
||||
'anxpro': require ('./js/anxpro.js'),
|
||||
'aofex': require ('./js/aofex.js'),
|
||||
'bcex': require ('./js/bcex.js'),
|
||||
'bequant': require ('./js/bequant.js'),
|
||||
'bibox': require ('./js/bibox.js'),
|
||||
'bigone': require ('./js/bigone.js'),
|
||||
'binance': require ('./js/binance.js'),
|
||||
'binanceje': require ('./js/binanceje.js'),
|
||||
'binanceus': require ('./js/binanceus.js'),
|
||||
'bit2c': require ('./js/bit2c.js'),
|
||||
'bitbank': require ('./js/bitbank.js'),
|
||||
'bitbay': require ('./js/bitbay.js'),
|
||||
'bitfinex': require ('./js/bitfinex.js'),
|
||||
'bitfinex2': require ('./js/bitfinex2.js'),
|
||||
'bitflyer': require ('./js/bitflyer.js'),
|
||||
'bitforex': require ('./js/bitforex.js'),
|
||||
'bithumb': require ('./js/bithumb.js'),
|
||||
'bitkk': require ('./js/bitkk.js'),
|
||||
'bitmart': require ('./js/bitmart.js'),
|
||||
'bitmax': require ('./js/bitmax.js'),
|
||||
'bitmex': require ('./js/bitmex.js'),
|
||||
'bitso': require ('./js/bitso.js'),
|
||||
'bitstamp': require ('./js/bitstamp.js'),
|
||||
'bitstamp1': require ('./js/bitstamp1.js'),
|
||||
'bittrex': require ('./js/bittrex.js'),
|
||||
'bitz': require ('./js/bitz.js'),
|
||||
'bl3p': require ('./js/bl3p.js'),
|
||||
'bleutrade': require ('./js/bleutrade.js'),
|
||||
'braziliex': require ('./js/braziliex.js'),
|
||||
'btcalpha': require ('./js/btcalpha.js'),
|
||||
'btcbox': require ('./js/btcbox.js'),
|
||||
'btcmarkets': require ('./js/btcmarkets.js'),
|
||||
'btctradeim': require ('./js/btctradeim.js'),
|
||||
'btctradeua': require ('./js/btctradeua.js'),
|
||||
'btcturk': require ('./js/btcturk.js'),
|
||||
'buda': require ('./js/buda.js'),
|
||||
'bw': require ('./js/bw.js'),
|
||||
'bybit': require ('./js/bybit.js'),
|
||||
'bytetrade': require ('./js/bytetrade.js'),
|
||||
'cex': require ('./js/cex.js'),
|
||||
'chilebit': require ('./js/chilebit.js'),
|
||||
'coinbase': require ('./js/coinbase.js'),
|
||||
'coinbaseprime': require ('./js/coinbaseprime.js'),
|
||||
'coinbasepro': require ('./js/coinbasepro.js'),
|
||||
'coincheck': require ('./js/coincheck.js'),
|
||||
'coinegg': require ('./js/coinegg.js'),
|
||||
'coinex': require ('./js/coinex.js'),
|
||||
'coinfalcon': require ('./js/coinfalcon.js'),
|
||||
'coinfloor': require ('./js/coinfloor.js'),
|
||||
'coingi': require ('./js/coingi.js'),
|
||||
'coinmarketcap': require ('./js/coinmarketcap.js'),
|
||||
'coinmate': require ('./js/coinmate.js'),
|
||||
'coinone': require ('./js/coinone.js'),
|
||||
'coinspot': require ('./js/coinspot.js'),
|
||||
'coolcoin': require ('./js/coolcoin.js'),
|
||||
'coss': require ('./js/coss.js'),
|
||||
'crex24': require ('./js/crex24.js'),
|
||||
'deribit': require ('./js/deribit.js'),
|
||||
'digifinex': require ('./js/digifinex.js'),
|
||||
'dsx': require ('./js/dsx.js'),
|
||||
'exmo': require ('./js/exmo.js'),
|
||||
'exx': require ('./js/exx.js'),
|
||||
'fcoin': require ('./js/fcoin.js'),
|
||||
'fcoinjp': require ('./js/fcoinjp.js'),
|
||||
'flowbtc': require ('./js/flowbtc.js'),
|
||||
'foxbit': require ('./js/foxbit.js'),
|
||||
'ftx': require ('./js/ftx.js'),
|
||||
'fybse': require ('./js/fybse.js'),
|
||||
'gateio': require ('./js/gateio.js'),
|
||||
'gemini': require ('./js/gemini.js'),
|
||||
'hbtc': require ('./js/hbtc.js'),
|
||||
'hitbtc': require ('./js/hitbtc.js'),
|
||||
'hollaex': require ('./js/hollaex.js'),
|
||||
'huobipro': require ('./js/huobipro.js'),
|
||||
'huobiru': require ('./js/huobiru.js'),
|
||||
'ice3x': require ('./js/ice3x.js'),
|
||||
'idex': require ('./js/idex.js'),
|
||||
'independentreserve': require ('./js/independentreserve.js'),
|
||||
'indodax': require ('./js/indodax.js'),
|
||||
'itbit': require ('./js/itbit.js'),
|
||||
'kkex': require ('./js/kkex.js'),
|
||||
'kraken': require ('./js/kraken.js'),
|
||||
'kucoin': require ('./js/kucoin.js'),
|
||||
'kuna': require ('./js/kuna.js'),
|
||||
'lakebtc': require ('./js/lakebtc.js'),
|
||||
'latoken': require ('./js/latoken.js'),
|
||||
'lbank': require ('./js/lbank.js'),
|
||||
'liquid': require ('./js/liquid.js'),
|
||||
'livecoin': require ('./js/livecoin.js'),
|
||||
'luno': require ('./js/luno.js'),
|
||||
'lykke': require ('./js/lykke.js'),
|
||||
'mercado': require ('./js/mercado.js'),
|
||||
'mixcoins': require ('./js/mixcoins.js'),
|
||||
'oceanex': require ('./js/oceanex.js'),
|
||||
'okcoin': require ('./js/okcoin.js'),
|
||||
'okex': require ('./js/okex.js'),
|
||||
'paymium': require ('./js/paymium.js'),
|
||||
'poloniex': require ('./js/poloniex.js'),
|
||||
'rightbtc': require ('./js/rightbtc.js'),
|
||||
'southxchange': require ('./js/southxchange.js'),
|
||||
'stex': require ('./js/stex.js'),
|
||||
'stronghold': require ('./js/stronghold.js'),
|
||||
'surbitcoin': require ('./js/surbitcoin.js'),
|
||||
'theocean': require ('./js/theocean.js'),
|
||||
'therock': require ('./js/therock.js'),
|
||||
'tidebit': require ('./js/tidebit.js'),
|
||||
'tidex': require ('./js/tidex.js'),
|
||||
'timex': require ('./js/timex.js'),
|
||||
'topq': require ('./js/topq.js'),
|
||||
'upbit': require ('./js/upbit.js'),
|
||||
'vaultoro': require ('./js/vaultoro.js'),
|
||||
'vbtc': require ('./js/vbtc.js'),
|
||||
'whitebit': require ('./js/whitebit.js'),
|
||||
'xbtce': require ('./js/xbtce.js'),
|
||||
'yobit': require ('./js/yobit.js'),
|
||||
'zaif': require ('./js/zaif.js'),
|
||||
'zb': require ('./js/zb.js'),
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
module.exports = Object.assign ({ version, Exchange, 'exchanges': Object.keys (exchanges) }, exchanges, functions, errors)
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
+129677
File diff suppressed because it is too large.
Load diff
+305
@@ -0,0 +1,305 @@
|
||||
'use strict';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const Exchange = require ('./base/Exchange');
|
||||
const { ExchangeError, ExchangeNotAvailable } = require ('./base/errors');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
module.exports = class _1btcxe extends Exchange {
|
||||
describe () {
|
||||
return this.deepExtend (super.describe (), {
|
||||
'id': '_1btcxe',
|
||||
'name': '1BTCXE',
|
||||
'countries': [ 'PA' ], // Panama
|
||||
'comment': 'Crypto Capital API',
|
||||
'has': {
|
||||
'CORS': true,
|
||||
'withdraw': true,
|
||||
},
|
||||
'timeframes': {
|
||||
'1d': '1year',
|
||||
},
|
||||
'urls': {
|
||||
'logo': 'https://user-images.githubusercontent.com/1294454/27766049-2b294408-5ecc-11e7-85cc-adaff013dc1a.jpg',
|
||||
'api': 'https://1btcxe.com/api',
|
||||
'www': 'https://1btcxe.com',
|
||||
'doc': 'https://1btcxe.com/api-docs.php',
|
||||
},
|
||||
'api': {
|
||||
'public': {
|
||||
'get': [
|
||||
'stats',
|
||||
'historical-prices',
|
||||
'order-book',
|
||||
'transactions',
|
||||
],
|
||||
},
|
||||
'private': {
|
||||
'post': [
|
||||
'balances-and-info',
|
||||
'open-orders',
|
||||
'user-transactions',
|
||||
'btc-deposit-address/get',
|
||||
'btc-deposit-address/new',
|
||||
'deposits/get',
|
||||
'withdrawals/get',
|
||||
'orders/new',
|
||||
'orders/edit',
|
||||
'orders/cancel',
|
||||
'orders/status',
|
||||
'withdrawals/new',
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async fetchMarkets (params = {}) {
|
||||
return [
|
||||
{ 'id': 'USD', 'symbol': 'BTC/USD', 'base': 'BTC', 'quote': 'USD', 'baseId': 'BTC', 'quoteId': 'USD' },
|
||||
{ 'id': 'EUR', 'symbol': 'BTC/EUR', 'base': 'BTC', 'quote': 'EUR', 'baseId': 'BTC', 'quoteId': 'EUR' },
|
||||
{ 'id': 'CNY', 'symbol': 'BTC/CNY', 'base': 'BTC', 'quote': 'CNY', 'baseId': 'BTC', 'quoteId': 'CNY' },
|
||||
{ 'id': 'RUB', 'symbol': 'BTC/RUB', 'base': 'BTC', 'quote': 'RUB', 'baseId': 'BTC', 'quoteId': 'RUB' },
|
||||
{ 'id': 'CHF', 'symbol': 'BTC/CHF', 'base': 'BTC', 'quote': 'CHF', 'baseId': 'BTC', 'quoteId': 'CHF' },
|
||||
{ 'id': 'JPY', 'symbol': 'BTC/JPY', 'base': 'BTC', 'quote': 'JPY', 'baseId': 'BTC', 'quoteId': 'JPY' },
|
||||
{ 'id': 'GBP', 'symbol': 'BTC/GBP', 'base': 'BTC', 'quote': 'GBP', 'baseId': 'BTC', 'quoteId': 'GBP' },
|
||||
{ 'id': 'CAD', 'symbol': 'BTC/CAD', 'base': 'BTC', 'quote': 'CAD', 'baseId': 'BTC', 'quoteId': 'CAD' },
|
||||
{ 'id': 'AUD', 'symbol': 'BTC/AUD', 'base': 'BTC', 'quote': 'AUD', 'baseId': 'BTC', 'quoteId': 'AUD' },
|
||||
{ 'id': 'AED', 'symbol': 'BTC/AED', 'base': 'BTC', 'quote': 'AED', 'baseId': 'BTC', 'quoteId': 'AED' },
|
||||
{ 'id': 'BGN', 'symbol': 'BTC/BGN', 'base': 'BTC', 'quote': 'BGN', 'baseId': 'BTC', 'quoteId': 'BGN' },
|
||||
{ 'id': 'CZK', 'symbol': 'BTC/CZK', 'base': 'BTC', 'quote': 'CZK', 'baseId': 'BTC', 'quoteId': 'CZK' },
|
||||
{ 'id': 'DKK', 'symbol': 'BTC/DKK', 'base': 'BTC', 'quote': 'DKK', 'baseId': 'BTC', 'quoteId': 'DKK' },
|
||||
{ 'id': 'HKD', 'symbol': 'BTC/HKD', 'base': 'BTC', 'quote': 'HKD', 'baseId': 'BTC', 'quoteId': 'HKD' },
|
||||
{ 'id': 'HRK', 'symbol': 'BTC/HRK', 'base': 'BTC', 'quote': 'HRK', 'baseId': 'BTC', 'quoteId': 'HRK' },
|
||||
{ 'id': 'HUF', 'symbol': 'BTC/HUF', 'base': 'BTC', 'quote': 'HUF', 'baseId': 'BTC', 'quoteId': 'HUF' },
|
||||
{ 'id': 'ILS', 'symbol': 'BTC/ILS', 'base': 'BTC', 'quote': 'ILS', 'baseId': 'BTC', 'quoteId': 'ILS' },
|
||||
{ 'id': 'INR', 'symbol': 'BTC/INR', 'base': 'BTC', 'quote': 'INR', 'baseId': 'BTC', 'quoteId': 'INR' },
|
||||
{ 'id': 'MUR', 'symbol': 'BTC/MUR', 'base': 'BTC', 'quote': 'MUR', 'baseId': 'BTC', 'quoteId': 'MUR' },
|
||||
{ 'id': 'MXN', 'symbol': 'BTC/MXN', 'base': 'BTC', 'quote': 'MXN', 'baseId': 'BTC', 'quoteId': 'MXN' },
|
||||
{ 'id': 'NOK', 'symbol': 'BTC/NOK', 'base': 'BTC', 'quote': 'NOK', 'baseId': 'BTC', 'quoteId': 'NOK' },
|
||||
{ 'id': 'NZD', 'symbol': 'BTC/NZD', 'base': 'BTC', 'quote': 'NZD', 'baseId': 'BTC', 'quoteId': 'NZD' },
|
||||
{ 'id': 'PLN', 'symbol': 'BTC/PLN', 'base': 'BTC', 'quote': 'PLN', 'baseId': 'BTC', 'quoteId': 'PLN' },
|
||||
{ 'id': 'RON', 'symbol': 'BTC/RON', 'base': 'BTC', 'quote': 'RON', 'baseId': 'BTC', 'quoteId': 'RON' },
|
||||
{ 'id': 'SEK', 'symbol': 'BTC/SEK', 'base': 'BTC', 'quote': 'SEK', 'baseId': 'BTC', 'quoteId': 'SEK' },
|
||||
{ 'id': 'SGD', 'symbol': 'BTC/SGD', 'base': 'BTC', 'quote': 'SGD', 'baseId': 'BTC', 'quoteId': 'SGD' },
|
||||
{ 'id': 'THB', 'symbol': 'BTC/THB', 'base': 'BTC', 'quote': 'THB', 'baseId': 'BTC', 'quoteId': 'THB' },
|
||||
{ 'id': 'TRY', 'symbol': 'BTC/TRY', 'base': 'BTC', 'quote': 'TRY', 'baseId': 'BTC', 'quoteId': 'TRY' },
|
||||
{ 'id': 'ZAR', 'symbol': 'BTC/ZAR', 'base': 'BTC', 'quote': 'ZAR', 'baseId': 'BTC', 'quoteId': 'ZAR' },
|
||||
];
|
||||
}
|
||||
|
||||
async fetchBalance (params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const response = await this.privatePostBalancesAndInfo (params);
|
||||
const balance = response['balances-and-info'];
|
||||
const result = { 'info': balance };
|
||||
const codes = Object.keys (this.currencies);
|
||||
for (let i = 0; i < codes.length; i++) {
|
||||
const code = codes[i];
|
||||
const currency = this.currency (code);
|
||||
const currencyId = currency['id'];
|
||||
const account = this.account ();
|
||||
account['free'] = this.safeFloat (balance['available'], currencyId);
|
||||
account['used'] = this.safeFloat (balance['on_hold'], currencyId);
|
||||
result[code] = account;
|
||||
}
|
||||
return this.parseBalance (result);
|
||||
}
|
||||
|
||||
async fetchOrderBook (symbol, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const request = {
|
||||
'currency': this.marketId (symbol),
|
||||
};
|
||||
const response = await this.publicGetOrderBook (this.extend (request, params));
|
||||
return this.parseOrderBook (response['order-book'], undefined, 'bid', 'ask', 'price', 'order_amount');
|
||||
}
|
||||
|
||||
async fetchTicker (symbol, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const request = {
|
||||
'currency': this.marketId (symbol),
|
||||
};
|
||||
const response = await this.publicGetStats (this.extend (request, params));
|
||||
const ticker = this.safeValue (response, 'stats', {});
|
||||
const last = this.safeFloat (ticker, 'last_price');
|
||||
return {
|
||||
'symbol': symbol,
|
||||
'timestamp': undefined,
|
||||
'datetime': undefined,
|
||||
'high': this.safeFloat (ticker, 'max'),
|
||||
'low': this.safeFloat (ticker, 'min'),
|
||||
'bid': this.safeFloat (ticker, 'bid'),
|
||||
'bidVolume': undefined,
|
||||
'ask': this.safeFloat (ticker, 'ask'),
|
||||
'askVolume': undefined,
|
||||
'vwap': undefined,
|
||||
'open': this.safeFloat (ticker, 'open'),
|
||||
'close': last,
|
||||
'last': last,
|
||||
'previousClose': undefined,
|
||||
'change': this.safeFloat (ticker, 'daily_change'),
|
||||
'percentage': undefined,
|
||||
'average': undefined,
|
||||
'baseVolume': undefined,
|
||||
'quoteVolume': this.safeFloat (ticker, 'total_btc_traded'),
|
||||
'info': ticker,
|
||||
};
|
||||
}
|
||||
|
||||
parseOHLCV (ohlcv, market = undefined, timeframe = '1d', since = undefined, limit = undefined) {
|
||||
return [
|
||||
this.parse8601 (ohlcv['date'] + ' 00:00:00'),
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
this.safeFloat (ohlcv, 'price'),
|
||||
undefined,
|
||||
];
|
||||
}
|
||||
|
||||
async fetchOHLCV (symbol, timeframe = '1d', since = undefined, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const response = await this.publicGetHistoricalPrices (this.extend ({
|
||||
'currency': market['id'],
|
||||
'timeframe': this.timeframes[timeframe],
|
||||
}, params));
|
||||
const ohlcvs = this.toArray (this.omit (response['historical-prices'], 'request_currency'));
|
||||
return this.parseOHLCVs (ohlcvs, market, timeframe, since, limit);
|
||||
}
|
||||
|
||||
parseTrade (trade, market = undefined) {
|
||||
const timestamp = this.safeTimestamp (trade, 'timestamp');
|
||||
const id = this.safeString (trade, 'id');
|
||||
let symbol = undefined;
|
||||
if (market !== undefined) {
|
||||
symbol = market['symbol'];
|
||||
}
|
||||
const type = undefined;
|
||||
const side = this.safeString (trade, 'maker_type');
|
||||
const price = this.safeFloat (trade, 'price');
|
||||
const amount = this.safeFloat (trade, 'amount');
|
||||
let cost = undefined;
|
||||
if (amount !== undefined) {
|
||||
if (price !== undefined) {
|
||||
cost = amount * price;
|
||||
}
|
||||
}
|
||||
return {
|
||||
'id': id,
|
||||
'info': trade,
|
||||
'timestamp': timestamp,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'symbol': symbol,
|
||||
'order': undefined,
|
||||
'type': type,
|
||||
'side': side,
|
||||
'takerOrMaker': undefined,
|
||||
'price': price,
|
||||
'amount': amount,
|
||||
'cost': cost,
|
||||
'fee': undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async fetchTrades (symbol, since = undefined, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const request = {
|
||||
'currency': market['id'],
|
||||
};
|
||||
if (limit !== undefined) {
|
||||
request['limit'] = limit;
|
||||
}
|
||||
const response = await this.publicGetTransactions (this.extend (request, params));
|
||||
const trades = this.toArray (this.omit (response['transactions'], 'request_currency'));
|
||||
return this.parseTrades (trades, market, since, limit);
|
||||
}
|
||||
|
||||
async createOrder (symbol, type, side, amount, price = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const request = {
|
||||
'side': side,
|
||||
'type': type,
|
||||
'currency': this.marketId (symbol),
|
||||
'amount': amount,
|
||||
};
|
||||
if (type === 'limit') {
|
||||
request['limit_price'] = price;
|
||||
}
|
||||
const result = await this.privatePostOrdersNew (this.extend (request, params));
|
||||
return {
|
||||
'info': result,
|
||||
'id': result,
|
||||
};
|
||||
}
|
||||
|
||||
async cancelOrder (id, symbol = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const request = {
|
||||
'id': id,
|
||||
};
|
||||
return await this.privatePostOrdersCancel (this.extend (request, params));
|
||||
}
|
||||
|
||||
async withdraw (code, amount, address, tag = undefined, params = {}) {
|
||||
this.checkAddress (address);
|
||||
await this.loadMarkets ();
|
||||
const currency = this.currency (code);
|
||||
const request = {
|
||||
'currency': currency['id'],
|
||||
'amount': parseFloat (amount),
|
||||
'address': address,
|
||||
};
|
||||
const response = await this.privatePostWithdrawalsNew (this.extend (request, params));
|
||||
return {
|
||||
'info': response,
|
||||
'id': response['result']['uuid'],
|
||||
};
|
||||
}
|
||||
|
||||
sign (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) {
|
||||
if (this.id === 'cryptocapital') {
|
||||
throw new ExchangeError (this.id + ' is an abstract base API for _1btcxe');
|
||||
}
|
||||
let url = this.urls['api'] + '/' + path;
|
||||
if (api === 'public') {
|
||||
if (Object.keys (params).length) {
|
||||
url += '?' + this.urlencode (params);
|
||||
}
|
||||
} else {
|
||||
this.checkRequiredCredentials ();
|
||||
const query = this.extend ({
|
||||
'api_key': this.apiKey,
|
||||
'nonce': this.nonce (),
|
||||
}, params);
|
||||
const request = this.json (query);
|
||||
query['signature'] = this.hmac (this.encode (request), this.encode (this.secret));
|
||||
body = this.json (query);
|
||||
headers = { 'Content-Type': 'application/json' };
|
||||
}
|
||||
return { 'url': url, 'method': method, 'body': body, 'headers': headers };
|
||||
}
|
||||
|
||||
async request (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) {
|
||||
const response = await this.fetch2 (path, api, method, params, headers, body);
|
||||
if (typeof response === 'string') {
|
||||
if (response.indexOf ('Maintenance') >= 0) {
|
||||
throw new ExchangeNotAvailable (this.id + ' on maintenance');
|
||||
}
|
||||
}
|
||||
if ('errors' in response) {
|
||||
let errors = [];
|
||||
for (let e = 0; e < response['errors'].length; e++) {
|
||||
const error = response['errors'][e];
|
||||
errors.push (error['code'] + ': ' + error['message']);
|
||||
}
|
||||
errors = errors.join (' ');
|
||||
throw new ExchangeError (this.id + ' ' + errors);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
};
|
||||
+473
@@ -0,0 +1,473 @@
|
||||
'use strict';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const Exchange = require ('./base/Exchange');
|
||||
const { InsufficientFunds, OrderNotFound } = require ('./base/errors');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
module.exports = class acx extends Exchange {
|
||||
describe () {
|
||||
return this.deepExtend (super.describe (), {
|
||||
'id': 'acx',
|
||||
'name': 'ACX',
|
||||
'countries': [ 'AU' ],
|
||||
'rateLimit': 1000,
|
||||
'version': 'v2',
|
||||
'has': {
|
||||
'CORS': true,
|
||||
'fetchTickers': true,
|
||||
'fetchOHLCV': true,
|
||||
'withdraw': true,
|
||||
'fetchOrder': true,
|
||||
},
|
||||
'timeframes': {
|
||||
'1m': '1',
|
||||
'5m': '5',
|
||||
'15m': '15',
|
||||
'30m': '30',
|
||||
'1h': '60',
|
||||
'2h': '120',
|
||||
'4h': '240',
|
||||
'12h': '720',
|
||||
'1d': '1440',
|
||||
'3d': '4320',
|
||||
'1w': '10080',
|
||||
},
|
||||
'urls': {
|
||||
'logo': 'https://user-images.githubusercontent.com/1294454/30247614-1fe61c74-9621-11e7-9e8c-f1a627afa279.jpg',
|
||||
'extension': '.json',
|
||||
'api': 'https://acx.io/api',
|
||||
'www': 'https://acx.io',
|
||||
'doc': 'https://acx.io/documents/api_v2',
|
||||
},
|
||||
'api': {
|
||||
'public': {
|
||||
'get': [
|
||||
'depth', // Get depth or specified market Both asks and bids are sorted from highest price to lowest.
|
||||
'k_with_pending_trades', // Get K data with pending trades, which are the trades not included in K data yet, because there's delay between trade generated and processed by K data generator
|
||||
'k', // Get OHLC(k line) of specific market
|
||||
'markets', // Get all available markets
|
||||
'order_book', // Get the order book of specified market
|
||||
'order_book/{market}',
|
||||
'tickers', // Get ticker of all markets
|
||||
'tickers/{market}', // Get ticker of specific market
|
||||
'timestamp', // Get server current time, in seconds since Unix epoch
|
||||
'trades', // Get recent trades on market, each trade is included only once Trades are sorted in reverse creation order.
|
||||
'trades/{market}',
|
||||
],
|
||||
},
|
||||
'private': {
|
||||
'get': [
|
||||
'members/me', // Get your profile and accounts info
|
||||
'deposits', // Get your deposits history
|
||||
'deposit', // Get details of specific deposit
|
||||
'deposit_address', // Where to deposit The address field could be empty when a new address is generating (e.g. for bitcoin), you should try again later in that case.
|
||||
'orders', // Get your orders, results is paginated
|
||||
'order', // Get information of specified order
|
||||
'trades/my', // Get your executed trades Trades are sorted in reverse creation order.
|
||||
'withdraws', // Get your cryptocurrency withdraws
|
||||
'withdraw', // Get your cryptocurrency withdraw
|
||||
],
|
||||
'post': [
|
||||
'orders', // Create a Sell/Buy order
|
||||
'orders/multi', // Create multiple sell/buy orders
|
||||
'orders/clear', // Cancel all my orders
|
||||
'order/delete', // Cancel an order
|
||||
'withdraw', // Create a withdraw
|
||||
],
|
||||
},
|
||||
},
|
||||
'fees': {
|
||||
'trading': {
|
||||
'tierBased': false,
|
||||
'percentage': true,
|
||||
'maker': 0.2 / 100,
|
||||
'taker': 0.2 / 100,
|
||||
},
|
||||
'funding': {
|
||||
'tierBased': false,
|
||||
'percentage': true,
|
||||
'withdraw': {}, // There is only 1% fee on withdrawals to your bank account.
|
||||
},
|
||||
},
|
||||
'commonCurrencies': {
|
||||
'PLA': 'Plair',
|
||||
},
|
||||
'exceptions': {
|
||||
'2002': InsufficientFunds,
|
||||
'2003': OrderNotFound,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async fetchMarkets (params = {}) {
|
||||
const markets = await this.publicGetMarkets (params);
|
||||
const result = [];
|
||||
for (let i = 0; i < markets.length; i++) {
|
||||
const market = markets[i];
|
||||
const id = market['id'];
|
||||
const symbol = market['name'];
|
||||
let baseId = this.safeString (market, 'base_unit');
|
||||
let quoteId = this.safeString (market, 'quote_unit');
|
||||
if ((baseId === undefined) || (quoteId === undefined)) {
|
||||
const ids = symbol.split ('/');
|
||||
baseId = ids[0].toLowerCase ();
|
||||
quoteId = ids[1].toLowerCase ();
|
||||
}
|
||||
let base = baseId.toUpperCase ();
|
||||
let quote = quoteId.toUpperCase ();
|
||||
base = this.safeCurrencyCode (base);
|
||||
quote = this.safeCurrencyCode (quote);
|
||||
// todo: find out their undocumented precision and limits
|
||||
const precision = {
|
||||
'amount': 8,
|
||||
'price': 8,
|
||||
};
|
||||
result.push ({
|
||||
'id': id,
|
||||
'symbol': symbol,
|
||||
'base': base,
|
||||
'quote': quote,
|
||||
'baseId': baseId,
|
||||
'quoteId': quoteId,
|
||||
'precision': precision,
|
||||
'info': market,
|
||||
'active': undefined,
|
||||
'limits': this.limits,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async fetchBalance (params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const response = await this.privateGetMembersMe (params);
|
||||
const balances = this.safeValue (response, 'accounts');
|
||||
const result = { 'info': balances };
|
||||
for (let i = 0; i < balances.length; i++) {
|
||||
const balance = balances[i];
|
||||
const currencyId = this.safeString (balance, 'currency');
|
||||
const code = this.safeCurrencyCode (currencyId);
|
||||
const account = this.account ();
|
||||
account['free'] = this.safeFloat (balance, 'balance');
|
||||
account['used'] = this.safeFloat (balance, 'locked');
|
||||
result[code] = account;
|
||||
}
|
||||
return this.parseBalance (result);
|
||||
}
|
||||
|
||||
async fetchOrderBook (symbol, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const request = {
|
||||
'market': market['id'],
|
||||
};
|
||||
if (limit !== undefined) {
|
||||
request['limit'] = limit; // default = 300
|
||||
}
|
||||
const orderbook = await this.publicGetDepth (this.extend (request, params));
|
||||
const timestamp = this.safeTimestamp (orderbook, 'timestamp');
|
||||
return this.parseOrderBook (orderbook, timestamp);
|
||||
}
|
||||
|
||||
parseTicker (ticker, market = undefined) {
|
||||
const timestamp = this.safeTimestamp (ticker, 'at');
|
||||
ticker = ticker['ticker'];
|
||||
let symbol = undefined;
|
||||
if (market) {
|
||||
symbol = market['symbol'];
|
||||
}
|
||||
const last = this.safeFloat (ticker, 'last');
|
||||
return {
|
||||
'symbol': symbol,
|
||||
'timestamp': timestamp,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'high': this.safeFloat (ticker, 'high'),
|
||||
'low': this.safeFloat (ticker, 'low'),
|
||||
'bid': this.safeFloat (ticker, 'buy'),
|
||||
'bidVolume': undefined,
|
||||
'ask': this.safeFloat (ticker, 'sell'),
|
||||
'askVolume': undefined,
|
||||
'vwap': undefined,
|
||||
'open': this.safeFloat (ticker, 'open'),
|
||||
'close': last,
|
||||
'last': last,
|
||||
'previousClose': undefined,
|
||||
'change': undefined,
|
||||
'percentage': undefined,
|
||||
'average': undefined,
|
||||
'baseVolume': this.safeFloat (ticker, 'vol'),
|
||||
'quoteVolume': undefined,
|
||||
'info': ticker,
|
||||
};
|
||||
}
|
||||
|
||||
async fetchTickers (symbols = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const response = await this.publicGetTickers (params);
|
||||
const ids = Object.keys (response);
|
||||
const result = {};
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
const id = ids[i];
|
||||
let market = undefined;
|
||||
let symbol = id;
|
||||
if (id in this.markets_by_id) {
|
||||
market = this.markets_by_id[id];
|
||||
symbol = market['symbol'];
|
||||
} else {
|
||||
let base = id.slice (0, 3);
|
||||
let quote = id.slice (3, 6);
|
||||
base = base.toUpperCase ();
|
||||
quote = quote.toUpperCase ();
|
||||
base = this.safeCurrencyCode (base);
|
||||
quote = this.safeCurrencyCode (quote);
|
||||
symbol = base + '/' + quote;
|
||||
}
|
||||
result[symbol] = this.parseTicker (response[id], market);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async fetchTicker (symbol, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const request = {
|
||||
'market': market['id'],
|
||||
};
|
||||
const response = await this.publicGetTickersMarket (this.extend (request, params));
|
||||
return this.parseTicker (response, market);
|
||||
}
|
||||
|
||||
parseTrade (trade, market = undefined) {
|
||||
const timestamp = this.parse8601 (this.safeString (trade, 'created_at'));
|
||||
const id = this.safeString (trade, 'tid');
|
||||
let symbol = undefined;
|
||||
if (market !== undefined) {
|
||||
symbol = market['symbol'];
|
||||
}
|
||||
return {
|
||||
'info': trade,
|
||||
'id': id,
|
||||
'timestamp': timestamp,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'symbol': symbol,
|
||||
'type': undefined,
|
||||
'side': undefined,
|
||||
'order': undefined,
|
||||
'takerOrMaker': undefined,
|
||||
'price': this.safeFloat (trade, 'price'),
|
||||
'amount': this.safeFloat (trade, 'volume'),
|
||||
'cost': this.safeFloat (trade, 'funds'),
|
||||
'fee': undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async fetchTrades (symbol, since = undefined, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const request = {
|
||||
'market': market['id'],
|
||||
};
|
||||
const response = await this.publicGetTrades (this.extend (request, params));
|
||||
return this.parseTrades (response, market, since, limit);
|
||||
}
|
||||
|
||||
parseOHLCV (ohlcv, market = undefined, timeframe = '1m', since = undefined, limit = undefined) {
|
||||
return [
|
||||
ohlcv[0] * 1000,
|
||||
ohlcv[1],
|
||||
ohlcv[2],
|
||||
ohlcv[3],
|
||||
ohlcv[4],
|
||||
ohlcv[5],
|
||||
];
|
||||
}
|
||||
|
||||
async fetchOHLCV (symbol, timeframe = '1m', since = undefined, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
if (limit === undefined) {
|
||||
limit = 500; // default is 30
|
||||
}
|
||||
const request = {
|
||||
'market': market['id'],
|
||||
'period': this.timeframes[timeframe],
|
||||
'limit': limit,
|
||||
};
|
||||
if (since !== undefined) {
|
||||
request['timestamp'] = parseInt (since / 1000);
|
||||
}
|
||||
const response = await this.publicGetK (this.extend (request, params));
|
||||
return this.parseOHLCVs (response, market, timeframe, since, limit);
|
||||
}
|
||||
|
||||
parseOrderStatus (status) {
|
||||
const statuses = {
|
||||
'done': 'closed',
|
||||
'wait': 'open',
|
||||
'cancel': 'canceled',
|
||||
};
|
||||
return this.safeString (statuses, status, status);
|
||||
}
|
||||
|
||||
parseOrder (order, market = undefined) {
|
||||
let symbol = undefined;
|
||||
if (market !== undefined) {
|
||||
symbol = market['symbol'];
|
||||
} else {
|
||||
const marketId = this.safeString (order, 'market');
|
||||
symbol = this.markets_by_id[marketId]['symbol'];
|
||||
}
|
||||
const timestamp = this.parse8601 (this.safeString (order, 'created_at'));
|
||||
const status = this.parseOrderStatus (this.safeString (order, 'state'));
|
||||
const type = this.safeString (order, 'type');
|
||||
const side = this.safeString (order, 'side');
|
||||
const id = this.safeString (order, 'id');
|
||||
return {
|
||||
'id': id,
|
||||
'clientOrderId': undefined,
|
||||
'timestamp': timestamp,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'lastTradeTimestamp': undefined,
|
||||
'status': status,
|
||||
'symbol': symbol,
|
||||
'type': type,
|
||||
'side': side,
|
||||
'price': this.safeFloat (order, 'price'),
|
||||
'amount': this.safeFloat (order, 'volume'),
|
||||
'filled': this.safeFloat (order, 'executed_volume'),
|
||||
'remaining': this.safeFloat (order, 'remaining_volume'),
|
||||
'trades': undefined,
|
||||
'fee': undefined,
|
||||
'info': order,
|
||||
'cost': undefined,
|
||||
'average': undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async fetchOrder (id, symbol = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const request = {
|
||||
'id': parseInt (id),
|
||||
};
|
||||
const response = await this.privateGetOrder (this.extend (request, params));
|
||||
return this.parseOrder (response);
|
||||
}
|
||||
|
||||
async createOrder (symbol, type, side, amount, price = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const request = {
|
||||
'market': this.marketId (symbol),
|
||||
'side': side,
|
||||
'volume': amount.toString (),
|
||||
'ord_type': type,
|
||||
};
|
||||
if (type === 'limit') {
|
||||
request['price'] = price.toString ();
|
||||
}
|
||||
const response = await this.privatePostOrders (this.extend (request, params));
|
||||
const marketId = this.safeValue (response, 'market');
|
||||
const market = this.safeValue (this.markets_by_id, marketId);
|
||||
return this.parseOrder (response, market);
|
||||
}
|
||||
|
||||
async cancelOrder (id, symbol = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const request = {
|
||||
'id': id,
|
||||
};
|
||||
const response = await this.privatePostOrderDelete (this.extend (request, params));
|
||||
const order = this.parseOrder (response);
|
||||
const status = order['status'];
|
||||
if (status === 'closed' || status === 'canceled') {
|
||||
throw new OrderNotFound (this.id + ' ' + this.json (order));
|
||||
}
|
||||
return order;
|
||||
}
|
||||
|
||||
async withdraw (code, amount, address, tag = undefined, params = {}) {
|
||||
this.checkAddress (address);
|
||||
await this.loadMarkets ();
|
||||
const currency = this.currency (code);
|
||||
// they have XRP but no docs on memo/tag
|
||||
const request = {
|
||||
'currency': currency['id'],
|
||||
'sum': amount,
|
||||
'address': address,
|
||||
};
|
||||
const response = await this.privatePostWithdraw (this.extend (request, params));
|
||||
// withdrawal response is undocumented
|
||||
return {
|
||||
'info': response,
|
||||
'id': undefined,
|
||||
};
|
||||
}
|
||||
|
||||
nonce () {
|
||||
return this.milliseconds ();
|
||||
}
|
||||
|
||||
encodeParams (params) {
|
||||
if ('orders' in params) {
|
||||
const orders = params['orders'];
|
||||
let query = this.urlencode (this.keysort (this.omit (params, 'orders')));
|
||||
for (let i = 0; i < orders.length; i++) {
|
||||
const order = orders[i];
|
||||
const keys = Object.keys (order);
|
||||
for (let k = 0; k < keys.length; k++) {
|
||||
const key = keys[k];
|
||||
const value = order[key];
|
||||
query += '&orders%5B%5D%5B' + key + '%5D=' + value.toString ();
|
||||
}
|
||||
}
|
||||
return query;
|
||||
}
|
||||
return this.urlencode (this.keysort (params));
|
||||
}
|
||||
|
||||
sign (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) {
|
||||
let request = '/api/' + this.version + '/' + this.implodeParams (path, params);
|
||||
if ('extension' in this.urls) {
|
||||
request += this.urls['extension'];
|
||||
}
|
||||
const query = this.omit (params, this.extractParams (path));
|
||||
let url = this.urls['api'] + request;
|
||||
if (api === 'public') {
|
||||
if (Object.keys (query).length) {
|
||||
url += '?' + this.urlencode (query);
|
||||
}
|
||||
} else {
|
||||
this.checkRequiredCredentials ();
|
||||
const nonce = this.nonce ().toString ();
|
||||
const query = this.encodeParams (this.extend ({
|
||||
'access_key': this.apiKey,
|
||||
'tonce': nonce,
|
||||
}, params));
|
||||
const auth = method + '|' + request + '|' + query;
|
||||
const signed = this.hmac (this.encode (auth), this.encode (this.secret));
|
||||
const suffix = query + '&signature=' + signed;
|
||||
if (method === 'GET') {
|
||||
url += '?' + suffix;
|
||||
} else {
|
||||
body = suffix;
|
||||
headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
|
||||
}
|
||||
}
|
||||
return { 'url': url, 'method': method, 'body': body, 'headers': headers };
|
||||
}
|
||||
|
||||
handleErrors (code, reason, url, method, headers, body, response, requestHeaders, requestBody) {
|
||||
if (response === undefined) {
|
||||
return;
|
||||
}
|
||||
if (code === 400) {
|
||||
const error = this.safeValue (response, 'error');
|
||||
const errorCode = this.safeString (error, 'code');
|
||||
const feedback = this.id + ' ' + this.json (response);
|
||||
this.throwExactlyMatchedException (this.exceptions, errorCode, feedback);
|
||||
// fallback to default error handler
|
||||
}
|
||||
}
|
||||
};
|
||||
+1221
File diff suppressed because it is too large.
Load diff
+1125
File diff suppressed because it is too large.
Load diff
+43
@@ -0,0 +1,43 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
"env": {
|
||||
"node": true,
|
||||
"es6": true,
|
||||
"browser": true,
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2017,
|
||||
"sourceType": "script",
|
||||
},
|
||||
"rules": {
|
||||
"semi": "off",
|
||||
"no-unused-vars": ["off"],
|
||||
"quotes": ["off", "single"],
|
||||
"quote-props": "off",
|
||||
"func-call-spacing": ["error", "always"],
|
||||
"one-var": "off",
|
||||
"indent": ["warn", 4, { "ignoredNodes": ["ObjectPattern"] }],
|
||||
"comma-style": "off",
|
||||
"no-multi-spaces": "off",
|
||||
"comma-dangle": "off",
|
||||
"spaced-comment": "off",
|
||||
"camelcase": "off",
|
||||
"padded-blocks": "off",
|
||||
"multiline-comment-style": "off",
|
||||
"curly": "off",
|
||||
"rest-spread-spacing": "off",
|
||||
"no-multiple-empty-lines": "off",
|
||||
"no-undef-init": "off",
|
||||
"no-useless-return": "off",
|
||||
"no-console": "off",
|
||||
"operator-linebreak": "off",
|
||||
"key-spacing": "off",
|
||||
"brace-style": "off",
|
||||
"padding-line-between-statements": ["off",
|
||||
{"blankLine": "never", "prev":"*", "next": "*" },
|
||||
{"blankLine": "always", "prev":"directive", "next": "*" },
|
||||
{"blankLine": "always", "prev":"*", "next": "cjs-export" },
|
||||
],
|
||||
},
|
||||
}
|
||||
+1484
File diff suppressed because it is too large.
Load diff
+44
@@ -0,0 +1,44 @@
|
||||
'use strict';
|
||||
|
||||
const errorHierarchy = {
|
||||
'BaseError': {
|
||||
'ExchangeError': {
|
||||
'AuthenticationError': {
|
||||
'PermissionDenied': {},
|
||||
'AccountSuspended': {},
|
||||
},
|
||||
'ArgumentsRequired': {},
|
||||
'BadRequest': {
|
||||
'BadSymbol': {},
|
||||
},
|
||||
'BadResponse': {
|
||||
'NullResponse': {},
|
||||
},
|
||||
'InsufficientFunds': {},
|
||||
'InvalidAddress': {
|
||||
'AddressPending': {},
|
||||
},
|
||||
'InvalidOrder': {
|
||||
'OrderNotFound': {},
|
||||
'OrderNotCached': {},
|
||||
'CancelPending': {},
|
||||
'OrderImmediatelyFillable': {},
|
||||
'OrderNotFillable': {},
|
||||
'DuplicateOrderId': {},
|
||||
},
|
||||
'NotSupported': {},
|
||||
},
|
||||
'NetworkError': {
|
||||
'DDoSProtection': {
|
||||
'RateLimitExceeded': {},
|
||||
},
|
||||
'ExchangeNotAvailable': {
|
||||
'OnMaintenance': {},
|
||||
},
|
||||
'InvalidNonce': {},
|
||||
'RequestTimeout': {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = errorHierarchy;
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
'use strict';
|
||||
|
||||
const errorHierarchy = require ('./errorHierarchy.js')
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
function subclass (BaseClass, classes, namespace = {}) {
|
||||
|
||||
for (const [className, subclasses] of Object.entries (classes)) {
|
||||
|
||||
const Class = Object.assign (namespace, {
|
||||
|
||||
/* By creating a named property, we trick compiler to assign our class constructor function a name.
|
||||
Otherwise, all our error constructors would be shown as [Function: Error] in the debugger! And
|
||||
the super-useful `e.constructor.name` magic wouldn't work — we then would have no chance to
|
||||
obtain a error type string from an error instance programmatically! */
|
||||
|
||||
[className]: class extends BaseClass {
|
||||
|
||||
constructor (message) {
|
||||
|
||||
super (message)
|
||||
|
||||
/* A workaround to make `instanceof` work on custom Error classes in transpiled ES5.
|
||||
See my blog post for the explanation of this hack:
|
||||
|
||||
https://medium.com/@xpl/javascript-deriving-from-error-properly-8d2f8f315801 */
|
||||
|
||||
this.constructor = Class
|
||||
this.__proto__ = Class.prototype
|
||||
this.name = className
|
||||
this.message = message
|
||||
}
|
||||
}
|
||||
|
||||
})[className]
|
||||
|
||||
subclass (Class, subclasses, namespace)
|
||||
}
|
||||
|
||||
return namespace
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
module.exports = subclass (
|
||||
// Root class
|
||||
Error,
|
||||
// Derived class hierarchy
|
||||
errorHierarchy
|
||||
)
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
'use strict';
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
const { unCamelCase } = require ('./functions/string')
|
||||
|
||||
const unCamelCasePropertyNames = x => {
|
||||
for (const k in x) x[unCamelCase (k)] = x[k] // camel_case_method = camelCaseMethod
|
||||
return x
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
module.exports = unCamelCasePropertyNames (Object.assign ({}
|
||||
|
||||
, require ('./functions/platform')
|
||||
, require ('./functions/generic')
|
||||
, require ('./functions/string')
|
||||
, require ('./functions/type')
|
||||
, require ('./functions/number')
|
||||
, require ('./functions/encode')
|
||||
, require ('./functions/crypto')
|
||||
, require ('./functions/time')
|
||||
, require ('./functions/throttle')
|
||||
, require ('./functions/misc')
|
||||
))
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
'use strict'
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
const CryptoJS = require ('../../static_dependencies/crypto-js/crypto-js')
|
||||
const { capitalize } = require ('./string')
|
||||
const { stringToBase64, utf16ToBase64, urlencodeBase64 } = require ('./encode')
|
||||
const NodeRSA = require ('./../../static_dependencies/node-rsa/NodeRSA')
|
||||
const { numberToLE } = require ('./encode')
|
||||
const EC = require ('./../../static_dependencies/elliptic/lib/elliptic').ec
|
||||
const { ArgumentsRequired } = require ('./../errors')
|
||||
const BN = require ('../../static_dependencies/BN/bn.js')
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
const hash = (request, hash = 'md5', digest = 'hex') => {
|
||||
const options = {}
|
||||
if (hash === 'keccak') {
|
||||
hash = 'SHA3'
|
||||
options['outputLength'] = 256
|
||||
}
|
||||
const result = CryptoJS[hash.toUpperCase ()] (request, options)
|
||||
return (digest === 'binary') ? result : result.toString (CryptoJS.enc[capitalize (digest)])
|
||||
}
|
||||
|
||||
/* ............................................. */
|
||||
|
||||
const hmac = (request, secret, hash = 'sha256', digest = 'hex') => {
|
||||
const result = CryptoJS['Hmac' + hash.toUpperCase ()] (request, secret)
|
||||
if (digest) {
|
||||
const encoding = (digest === 'binary') ? 'Latin1' : capitalize (digest)
|
||||
return result.toString (CryptoJS.enc[capitalize (encoding)])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/* ............................................. */
|
||||
|
||||
function rsa (request, secret, alg = 'RS256') {
|
||||
const algos = {
|
||||
'RS256': 'pkcs1-sha256',
|
||||
'RS512': 'pkcs1-sha512',
|
||||
}
|
||||
if (!(alg in algos)) {
|
||||
throw new ExchangeError (alg + ' is not a supported rsa signing algorithm.')
|
||||
}
|
||||
const algorithm = algos[alg]
|
||||
let key = new NodeRSA (secret, {
|
||||
'environment': 'browser',
|
||||
'signingScheme': algorithm,
|
||||
})
|
||||
return key.sign (request, 'base64', 'binary')
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
function jwt (request, secret, alg = 'HS256') {
|
||||
const algos = {
|
||||
'HS256': 'sha256',
|
||||
'HS384': 'sha384',
|
||||
'HS512': 'sha512',
|
||||
};
|
||||
const encodedHeader = urlencodeBase64 (stringToBase64 (JSON.stringify ({ 'alg': alg, 'typ': 'JWT' })))
|
||||
const encodedData = urlencodeBase64 (stringToBase64 (JSON.stringify (request)))
|
||||
const token = [ encodedHeader, encodedData ].join ('.')
|
||||
const algoType = alg.slice (0, 2);
|
||||
const algorithm = algos[alg]
|
||||
let signature = undefined
|
||||
if (algoType === 'HS') {
|
||||
signature = urlencodeBase64 (hmac (token, secret, algorithm, 'base64'))
|
||||
} else if (algoType === 'RS') {
|
||||
signature = urlencodeBase64 (rsa (token, secret, alg))
|
||||
}
|
||||
return [ token, signature ].join ('.')
|
||||
}
|
||||
|
||||
function ecdsa (request, secret, algorithm = 'p256', hashFunction = undefined, fixedLength = false) {
|
||||
let digest = request
|
||||
if (hashFunction !== undefined) {
|
||||
digest = hash (request, hashFunction, 'hex')
|
||||
}
|
||||
const curve = new EC (algorithm)
|
||||
let signature = curve.sign (digest, secret, 'hex', { 'canonical': true })
|
||||
let counter = new BN ('0')
|
||||
const minimum_size = new BN ('1').shln (8 * 31).sub (new BN ('1'))
|
||||
while (fixedLength && (signature.r.gt (curve.nh) || signature.r.lte (minimum_size) || signature.s.lte (minimum_size))) {
|
||||
signature = curve.sign (digest, secret, 'hex', { 'canonical': true, 'extraEntropy': counter.toArray ('le', 32)})
|
||||
counter = counter.add (new BN ('1'))
|
||||
}
|
||||
return {
|
||||
'r': signature.r.toString (16).padStart (64, '0'),
|
||||
's': signature.s.toString (16).padStart (64, '0'),
|
||||
'v': signature.recoveryParam,
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
const totp = (secret) => {
|
||||
|
||||
const dec2hex = s => ((s < 15.5 ? '0' : '') + Math.round (s).toString (16))
|
||||
, hex2dec = s => parseInt (s, 16)
|
||||
, leftpad = (s, p) => (p + s).slice (-p.length) // both s and p are short strings
|
||||
|
||||
const base32tohex = (base32) => {
|
||||
let base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
|
||||
let bits = ''
|
||||
let hex = ''
|
||||
for (let i = 0; i < base32.length; i++) {
|
||||
let val = base32chars.indexOf (base32.charAt (i).toUpperCase ())
|
||||
bits += leftpad (val.toString (2), '00000')
|
||||
}
|
||||
for (let i = 0; i + 4 <= bits.length; i += 4) {
|
||||
let chunk = bits.substr (i, 4)
|
||||
hex = hex + parseInt (chunk, 2).toString (16)
|
||||
}
|
||||
return hex
|
||||
}
|
||||
|
||||
const getOTP = (secret) => {
|
||||
secret = secret.replace (' ', '') // support 2fa-secrets with spaces like "4TDV WOGO" → "4TDVWOGO"
|
||||
let epoch = Math.round (new Date ().getTime () / 1000.0)
|
||||
let time = leftpad (dec2hex (Math.floor (epoch / 30)), '0000000000000000')
|
||||
let hmacRes = hmac (CryptoJS.enc.Hex.parse (time), CryptoJS.enc.Hex.parse (base32tohex (secret)), 'sha1', 'hex')
|
||||
let offset = hex2dec (hmacRes.substring (hmacRes.length - 1))
|
||||
let otp = (hex2dec (hmacRes.substr (offset * 2, 8)) & hex2dec ('7fffffff')) + ''
|
||||
otp = (otp).substr (otp.length - 6, 6)
|
||||
return otp
|
||||
}
|
||||
|
||||
return getOTP (secret)
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
module.exports = {
|
||||
hash,
|
||||
hmac,
|
||||
jwt,
|
||||
totp,
|
||||
rsa,
|
||||
ecdsa,
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
"use strict";
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
const CryptoJS = require ('../../static_dependencies/crypto-js/crypto-js')
|
||||
const qs = require ('../../static_dependencies/qs/index')
|
||||
const BN = require ('../../static_dependencies/BN/bn')
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
module.exports =
|
||||
|
||||
{ json: (data, params = undefined) => JSON.stringify (data)
|
||||
, unjson: JSON.parse
|
||||
|
||||
, isJsonEncodedObject: object => (
|
||||
(typeof object === 'string') &&
|
||||
(object.length >= 2) &&
|
||||
((object[0] === '{') || (object[0] === '['))
|
||||
)
|
||||
|
||||
, stringToBinary (str) {
|
||||
const arr = new Uint8Array (str.length)
|
||||
for (let i = 0; i < str.length; i++) { arr[i] = str.charCodeAt (i); }
|
||||
return CryptoJS.lib.WordArray.create (arr)
|
||||
}
|
||||
|
||||
, stringToBase64: string => CryptoJS.enc.Latin1.parse (string).toString (CryptoJS.enc.Base64)
|
||||
, utf16ToBase64: string => CryptoJS.enc.Utf16 .parse (string).toString (CryptoJS.enc.Base64)
|
||||
, base64ToBinary: string => CryptoJS.enc.Base64.parse (string)
|
||||
, base64ToString: string => CryptoJS.enc.Base64.parse (string).toString (CryptoJS.enc.Utf8)
|
||||
, binaryToBase64: binary => binary.toString (CryptoJS.enc.Base64)
|
||||
, base16ToBinary: string => CryptoJS.enc.Hex.parse (string)
|
||||
, binaryToBase16: binary => binary.toString (CryptoJS.enc.Hex)
|
||||
, binaryConcat: (...args) => args.reduce ((a, b) => a.concat (b))
|
||||
, binaryConcatArray: (arr) => arr.reduce ((a, b) => a.concat (b))
|
||||
|
||||
, urlencode: object => qs.stringify (object)
|
||||
, urlencodeWithArrayRepeat: object => qs.stringify (object, { arrayFormat: 'repeat' })
|
||||
, rawencode: object => qs.stringify (object, { encode: false })
|
||||
, encode: x => x
|
||||
, decode: x => x
|
||||
|
||||
// Url-safe-base64 without equals signs, with + replaced by - and slashes replaced by underscores
|
||||
|
||||
, urlencodeBase64: base64string => base64string.replace (/[=]+$/, '')
|
||||
.replace (/\+/g, '-')
|
||||
.replace (/\//g, '_')
|
||||
|
||||
, numberToLE: (n, padding) => {
|
||||
const hexArray = new BN (n).toArray ('le', padding)
|
||||
return byteArrayToWordArray (hexArray)
|
||||
}
|
||||
|
||||
, numberToBE: (n, padding) => {
|
||||
const hexArray = new BN (n).toArray ('be', padding)
|
||||
return byteArrayToWordArray (hexArray)
|
||||
}
|
||||
}
|
||||
|
||||
function byteArrayToWordArray(ba) {
|
||||
const wa = []
|
||||
for (let i = 0; i < ba.length; i++) {
|
||||
wa[(i / 4) | 0] |= ba[i] << (24 - 8 * i)
|
||||
}
|
||||
return CryptoJS.lib.WordArray.create (wa, ba.length)
|
||||
}
|
||||
|
||||
module.exports['byteArrayToWordArray'] = byteArrayToWordArray
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
"use strict";
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
const { isObject, isNumber, isDictionary, isArray } = require ('./type')
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
const keys = Object.keys
|
||||
|
||||
, values = x => !isArray (x) // don't copy arrays if they're already arrays!
|
||||
? Object.values (x)
|
||||
: x
|
||||
|
||||
, index = x => new Set (values (x))
|
||||
|
||||
, extend = (...args) => Object.assign ({}, ...args) // NB: side-effect free
|
||||
|
||||
, clone = x => isArray (x)
|
||||
? Array.from (x) // clones arrays
|
||||
: extend (x) // clones objects
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
module.exports =
|
||||
|
||||
{ keys
|
||||
, values
|
||||
, extend
|
||||
, clone
|
||||
, index
|
||||
, ordered: x => x // a stub to keep assoc keys in order (in JS it does nothing, it's mostly for Python)
|
||||
, unique: x => Array.from (index (x))
|
||||
, arrayConcat: (a, b) => a.concat (b)
|
||||
|
||||
/* ............................................. */
|
||||
|
||||
, inArray (needle, haystack) {
|
||||
|
||||
return haystack.includes (needle)
|
||||
}
|
||||
|
||||
, toArray (object) {
|
||||
|
||||
return Object.values (object)
|
||||
}
|
||||
|
||||
, isEmpty (object) {
|
||||
if (!object)
|
||||
return true;
|
||||
return (Array.isArray (object) ? object : Object.keys (object)).length < 1;
|
||||
}
|
||||
|
||||
/* ............................................. */
|
||||
|
||||
, keysort (x, out = {}) {
|
||||
|
||||
for (const k of keys (x).sort ())
|
||||
out[k] = x[k]
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
/* ............................................. */
|
||||
|
||||
/*
|
||||
Accepts a map/array of objects and a key name to be used as an index:
|
||||
array = [
|
||||
{ someKey: 'value1', anotherKey: 'anotherValue1' },
|
||||
{ someKey: 'value2', anotherKey: 'anotherValue2' },
|
||||
{ someKey: 'value3', anotherKey: 'anotherValue3' },
|
||||
]
|
||||
key = 'someKey'
|
||||
|
||||
Returns a map:
|
||||
{
|
||||
value1: { someKey: 'value1', anotherKey: 'anotherValue1' },
|
||||
value2: { someKey: 'value2', anotherKey: 'anotherValue2' },
|
||||
value3: { someKey: 'value3', anotherKey: 'anotherValue3' },
|
||||
}
|
||||
*/
|
||||
|
||||
, indexBy (x, k, out = {}) {
|
||||
|
||||
for (const v of values (x))
|
||||
if (k in v)
|
||||
out[v[k]] = v
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
/* ............................................. */
|
||||
|
||||
/*
|
||||
Accepts a map/array of objects and a key name to be used as a grouping parameter:
|
||||
array = [
|
||||
{ someKey: 'value1', anotherKey: 'anotherValue1' },
|
||||
{ someKey: 'value1', anotherKey: 'anotherValue2' },
|
||||
{ someKey: 'value3', anotherKey: 'anotherValue3' },
|
||||
]
|
||||
key = 'someKey'
|
||||
|
||||
Returns a map:
|
||||
{
|
||||
value1: [
|
||||
{ someKey: 'value1', anotherKey: 'anotherValue1' },
|
||||
{ someKey: 'value1', anotherKey: 'anotherValue2' },
|
||||
]
|
||||
value3: [
|
||||
{ someKey: 'value3', anotherKey: 'anotherValue3' }
|
||||
],
|
||||
}
|
||||
*/
|
||||
|
||||
, groupBy (x, k, out = {}) {
|
||||
|
||||
for (const v of values (x)) {
|
||||
if (k in v) {
|
||||
const p = v[k]
|
||||
out[p] = out[p] || []
|
||||
out[p].push (v)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/* ............................................. */
|
||||
|
||||
/*
|
||||
Accepts a map/array of objects, a key name and a key value to be used as a filter:
|
||||
array = [
|
||||
{ someKey: 'value1', anotherKey: 'anotherValue1' },
|
||||
{ someKey: 'value2', anotherKey: 'anotherValue2' },
|
||||
{ someKey: 'value3', anotherKey: 'anotherValue3' },
|
||||
]
|
||||
key = 'someKey'
|
||||
value = 'value1'
|
||||
|
||||
Returns an array:
|
||||
[
|
||||
value1: { someKey: 'value1', anotherKey: 'anotherValue1' },
|
||||
]
|
||||
*/
|
||||
|
||||
, filterBy (x, k, value = undefined, out = []) {
|
||||
|
||||
for (const v of values (x))
|
||||
if (v[k] === value)
|
||||
out.push (v)
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
/* ............................................. */
|
||||
|
||||
, sortBy: (array, // NB: MUTATES ARRAY!
|
||||
key,
|
||||
descending = false,
|
||||
direction = descending ? -1 : 1) => array.sort ((a, b) =>
|
||||
((a[key] < b[key]) ? -direction :
|
||||
((a[key] > b[key]) ? direction : 0)))
|
||||
|
||||
/* ............................................. */
|
||||
|
||||
, flatten: function flatten (x, out = []) {
|
||||
|
||||
for (const v of x) {
|
||||
if (isArray (v)) flatten (v, out)
|
||||
else out.push (v)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
/* ............................................. */
|
||||
|
||||
, pluck: (x, k) => values (x)
|
||||
.filter (v => k in v)
|
||||
.map (v => v[k])
|
||||
|
||||
/* ............................................. */
|
||||
|
||||
, omit (x, ...args) {
|
||||
|
||||
if (!Array.isArray (x)) {
|
||||
|
||||
const out = clone (x)
|
||||
|
||||
for (const k of args) {
|
||||
|
||||
if (isArray (k)) { // omit (x, ['a', 'b'])
|
||||
|
||||
for (const kk of k) {
|
||||
delete out[kk]
|
||||
}
|
||||
}
|
||||
|
||||
else delete out[k] // omit (x, 'a', 'b')
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
return x
|
||||
}
|
||||
|
||||
/* ............................................. */
|
||||
|
||||
, sum (...xs) {
|
||||
|
||||
const ns = xs.filter (isNumber) // leave only numbers
|
||||
|
||||
return (ns.length > 0)
|
||||
? ns.reduce ((a, b) => a + b, 0)
|
||||
: undefined
|
||||
}
|
||||
|
||||
/* ............................................. */
|
||||
|
||||
, deepExtend: function deepExtend (...xs) {
|
||||
let out = undefined
|
||||
for (const x of xs) {
|
||||
if (isDictionary (x)) {
|
||||
if (!isObject (out)) {
|
||||
out = {}
|
||||
}
|
||||
for (const k in x) {
|
||||
out[k] = deepExtend (out[k], x[k])
|
||||
}
|
||||
} else {
|
||||
out = x
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
}
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
'use strict';
|
||||
|
||||
const { ROUND_UP, ROUND_DOWN } = require ('./number')
|
||||
const { NotSupported } = require ('./../errors')
|
||||
|
||||
//-------------------------------------------------------------------------
|
||||
// converts timeframe to seconds
|
||||
const parseTimeframe = (timeframe) => {
|
||||
|
||||
const amount = timeframe.slice (0, -1)
|
||||
const unit = timeframe.slice (-1)
|
||||
let scale = undefined;
|
||||
|
||||
if (unit === 'y') {
|
||||
scale = 60 * 60 * 24 * 365
|
||||
} else if (unit === 'M') {
|
||||
scale = 60 * 60 * 24 * 30
|
||||
} else if (unit === 'w') {
|
||||
scale = 60 * 60 * 24 * 7
|
||||
} else if (unit === 'd') {
|
||||
scale = 60 * 60 * 24
|
||||
} else if (unit === 'h') {
|
||||
scale = 60 * 60
|
||||
} else if (unit === 'm') {
|
||||
scale = 60
|
||||
} else if (unit === 's') {
|
||||
scale = 1
|
||||
} else {
|
||||
throw new NotSupported ('timeframe unit ' + unit + ' is not supported')
|
||||
}
|
||||
|
||||
return amount * scale
|
||||
}
|
||||
|
||||
const roundTimeframe = (timeframe, timestamp, direction = ROUND_DOWN) => {
|
||||
const ms = parseTimeframe (timeframe) * 1000
|
||||
// Get offset based on timeframe in milliseconds
|
||||
const offset = timestamp % ms
|
||||
return timestamp - offset + ((direction === ROUND_UP) ? ms : 0);
|
||||
}
|
||||
|
||||
// given a sorted arrays of trades (recent last) and a timeframe builds an array of OHLCV candles
|
||||
const buildOHLCVC = (trades, timeframe = '1m', since = -Infinity, limit = Infinity) => {
|
||||
const ms = parseTimeframe (timeframe) * 1000;
|
||||
const ohlcvs = [];
|
||||
const [ timestamp, /* open */, high, low, close, volume, count ] = [ 0, 1, 2, 3, 4, 5, 6 ];
|
||||
const oldest = Math.min (trades.length - 1, limit);
|
||||
|
||||
for (let i = 0; i <= oldest; i++) {
|
||||
const trade = trades[i];
|
||||
if (trade.timestamp < since)
|
||||
continue;
|
||||
const openingTime = Math.floor (trade.timestamp / ms) * ms; // shift to the edge of m/h/d (but not M)
|
||||
const candle = ohlcvs.length - 1;
|
||||
|
||||
if (candle === -1 || openingTime >= ohlcvs[candle][timestamp] + ms) {
|
||||
// moved to a new timeframe -> create a new candle from opening trade
|
||||
ohlcvs.push ([
|
||||
openingTime, // timestamp
|
||||
trade.price, // O
|
||||
trade.price, // H
|
||||
trade.price, // L
|
||||
trade.price, // C
|
||||
trade.amount, // V
|
||||
1, // count
|
||||
]);
|
||||
} else {
|
||||
// still processing the same timeframe -> update opening trade
|
||||
ohlcvs[candle][high] = Math.max (ohlcvs[candle][high], trade.price);
|
||||
ohlcvs[candle][low] = Math.min (ohlcvs[candle][low], trade.price);
|
||||
ohlcvs[candle][close] = trade.price;
|
||||
ohlcvs[candle][volume] += trade.amount;
|
||||
ohlcvs[candle][count]++;
|
||||
} // if
|
||||
} // for
|
||||
return ohlcvs;
|
||||
}
|
||||
|
||||
const extractParams = (string) => {
|
||||
const re = /{([\w-]+)}/g
|
||||
const matches = []
|
||||
let match = re.exec (string)
|
||||
while (match) {
|
||||
matches.push (match[1])
|
||||
match = re.exec (string)
|
||||
}
|
||||
return matches
|
||||
}
|
||||
|
||||
const implodeParams = (string, params) => {
|
||||
if (!Array.isArray (params)) {
|
||||
const keys = Object.keys (params)
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i]
|
||||
if (!Array.isArray (params[key])) {
|
||||
string = string.replace ('{' + key + '}', params[key])
|
||||
}
|
||||
}
|
||||
}
|
||||
return string
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
module.exports = {
|
||||
|
||||
aggregate (bidasks) {
|
||||
|
||||
const result = {}
|
||||
|
||||
for (let i = 0; i < bidasks.length; i++) {
|
||||
const [ price, volume ] = bidasks[i];
|
||||
if (volume > 0) {
|
||||
result[price] = (result[price] || 0) + volume
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys (result).map (price => [parseFloat (price), parseFloat (result[price])])
|
||||
},
|
||||
|
||||
parseTimeframe,
|
||||
roundTimeframe,
|
||||
buildOHLCVC,
|
||||
ROUND_UP,
|
||||
ROUND_DOWN,
|
||||
|
||||
implodeParams,
|
||||
extractParams
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
+335
@@ -0,0 +1,335 @@
|
||||
'use strict'
|
||||
|
||||
/* ------------------------------------------------------------------------
|
||||
|
||||
NB: initially, I used objects for options passing:
|
||||
|
||||
decimalToPrecision ('123.456', { digits: 2, round: true, afterPoint: true })
|
||||
|
||||
...but it turns out it's hard to port that across different languages and it is also
|
||||
probably has a performance penalty -- while it's a performance critical code! So
|
||||
I switched to using named constants instead, as it is actually more readable and
|
||||
succinct, and surely doesn't come with any inherent performance downside:
|
||||
|
||||
decimalToPrecision ('123.456', ROUND, 2, DECIMAL_PLACES) */
|
||||
|
||||
const ROUND = 0 // rounding mode
|
||||
, TRUNCATE = 1
|
||||
, ROUND_UP = 2
|
||||
, ROUND_DOWN = 3
|
||||
|
||||
const DECIMAL_PLACES = 0 // digits counting mode
|
||||
, SIGNIFICANT_DIGITS = 1
|
||||
, TICK_SIZE = 2
|
||||
|
||||
const NO_PADDING = 0 // zero-padding mode
|
||||
, PAD_WITH_ZERO = 1
|
||||
|
||||
const precisionConstants = {
|
||||
ROUND,
|
||||
TRUNCATE,
|
||||
ROUND_UP,
|
||||
ROUND_DOWN,
|
||||
DECIMAL_PLACES,
|
||||
SIGNIFICANT_DIGITS,
|
||||
TICK_SIZE,
|
||||
NO_PADDING,
|
||||
PAD_WITH_ZERO,
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
// See https://stackoverflow.com/questions/1685680/how-to-avoid-scientific-notation-for-large-numbers-in-javascript for discussion
|
||||
|
||||
function numberToString (x) { // avoids scientific notation for too large and too small numbers
|
||||
|
||||
if (typeof x === 'string') return x
|
||||
|
||||
const s = x.toString ()
|
||||
if (Math.abs (x) < 1.0) {
|
||||
const e = parseInt (s.split ('e-')[1])
|
||||
const neg = (s[0] === '-')
|
||||
if (e) {
|
||||
x *= Math.pow (10, e - 1)
|
||||
x = (neg ? '-' : '') + '0.' + (new Array (e)).join ('0') + x.toString ().substring (neg ? 3 : 2)
|
||||
return x
|
||||
}
|
||||
} else {
|
||||
const parts = s.split ('e')
|
||||
if (parts[1]) {
|
||||
let e = parseInt (parts[1])
|
||||
const m = parts[0].split ('.')
|
||||
if (m[1]) {
|
||||
e -= m[1].length
|
||||
}
|
||||
return m[0] + m[1] + (new Array (e + 1)).join ('0')
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// expects non-scientific notation
|
||||
|
||||
const truncate_regExpCache = []
|
||||
, truncate_to_string = (num, precision = 0) => {
|
||||
num = numberToString (num)
|
||||
if (precision > 0) {
|
||||
const re = truncate_regExpCache[precision] || (truncate_regExpCache[precision] = new RegExp ("([-]*\\d+\\.\\d{" + precision + "})(\\d)"))
|
||||
const [ , result] = num.toString ().match (re) || [null, num]
|
||||
return result.toString ()
|
||||
}
|
||||
return parseInt (num).toString ()
|
||||
}
|
||||
, truncate = (num, precision = 0) => parseFloat (truncate_to_string (num, precision))
|
||||
|
||||
function precisionFromString (string) {
|
||||
const split = string.replace (/0+$/g, '').split ('.')
|
||||
return (split.length > 1) ? (split[1].length) : 0
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
const decimalToPrecision = (x, roundingMode
|
||||
, numPrecisionDigits
|
||||
, countingMode = DECIMAL_PLACES
|
||||
, paddingMode = NO_PADDING) => {
|
||||
|
||||
if (numPrecisionDigits < 0) {
|
||||
if (countingMode === TICK_SIZE) {
|
||||
throw new Error (`TICK_SIZE cant be used with negative numPrecisionDigits`)
|
||||
}
|
||||
const toNearest = Math.pow (10, -numPrecisionDigits)
|
||||
if (roundingMode === ROUND) {
|
||||
return (toNearest * decimalToPrecision (x / toNearest, roundingMode, 0, countingMode, paddingMode)).toString ()
|
||||
}
|
||||
if (roundingMode === TRUNCATE) {
|
||||
return (x - (x % toNearest)).toString ()
|
||||
}
|
||||
}
|
||||
|
||||
/* handle tick size */
|
||||
if (countingMode === TICK_SIZE) {
|
||||
const precisionDigitsString = decimalToPrecision (numPrecisionDigits, ROUND, 100, DECIMAL_PLACES, NO_PADDING)
|
||||
const newNumPrecisionDigits = precisionFromString (precisionDigitsString)
|
||||
let missing = x % numPrecisionDigits
|
||||
// See: https://github.com/ccxt/ccxt/pull/6486
|
||||
missing = Number (decimalToPrecision (missing, ROUND, 8, DECIMAL_PLACES, NO_PADDING));
|
||||
const fpError = decimalToPrecision (missing / numPrecisionDigits, ROUND, Math.max (newNumPrecisionDigits, 8), DECIMAL_PLACES, NO_PADDING)
|
||||
if (precisionFromString (fpError) !== 0) {
|
||||
if (roundingMode === ROUND) {
|
||||
if (x > 0) {
|
||||
if (missing >= numPrecisionDigits / 2) {
|
||||
x = x - missing + numPrecisionDigits
|
||||
} else {
|
||||
x = x - missing
|
||||
}
|
||||
} else {
|
||||
if (missing >= numPrecisionDigits / 2) {
|
||||
x = Number (x) - missing
|
||||
} else {
|
||||
x = Number (x) - missing - numPrecisionDigits
|
||||
}
|
||||
}
|
||||
} else if (roundingMode === TRUNCATE) {
|
||||
x = x - missing
|
||||
}
|
||||
}
|
||||
return decimalToPrecision (x, ROUND, newNumPrecisionDigits, DECIMAL_PLACES, paddingMode);
|
||||
}
|
||||
|
||||
/* Convert to a string (if needed), skip leading minus sign (if any) */
|
||||
|
||||
const str = numberToString (x)
|
||||
, isNegative = str[0] === '-'
|
||||
, strStart = isNegative ? 1 : 0
|
||||
, strEnd = str.length
|
||||
|
||||
/* Find the dot position in the source buffer */
|
||||
|
||||
for (var strDot = 0; strDot < strEnd; strDot++)
|
||||
if (str[strDot] === '.')
|
||||
break
|
||||
|
||||
const hasDot = strDot < str.length
|
||||
|
||||
/* Char code constants */
|
||||
|
||||
const MINUS = 45
|
||||
, DOT = 46
|
||||
, ZERO = 48
|
||||
, ONE = (ZERO + 1)
|
||||
, FIVE = (ZERO + 5)
|
||||
, NINE = (ZERO + 9)
|
||||
|
||||
/* For -123.4567 the `chars` array will hold 01234567 (leading zero is reserved for rounding cases when 099 → 100) */
|
||||
|
||||
const chars = new Uint8Array ((strEnd - strStart) + (hasDot ? 0 : 1))
|
||||
chars[0] = ZERO
|
||||
|
||||
/* Validate & copy digits, determine certain locations in the resulting buffer */
|
||||
|
||||
let afterDot = chars.length
|
||||
, digitsStart = -1 // significant digits
|
||||
, digitsEnd = -1
|
||||
|
||||
for (var i = 1, j = strStart; j < strEnd; j++, i++) {
|
||||
|
||||
const c = str.charCodeAt (j)
|
||||
|
||||
if (c === DOT) {
|
||||
afterDot = i--
|
||||
|
||||
} else if ((c < ZERO) || (c > NINE)) {
|
||||
throw new Error (`${str}: invalid number (contains an illegal character '${str[i - 1]}')`)
|
||||
|
||||
} else {
|
||||
chars[i] = c
|
||||
if ((c !== ZERO) && (digitsStart < 0)) digitsStart = i
|
||||
}
|
||||
}
|
||||
|
||||
if (digitsStart < 0) digitsStart = 1
|
||||
|
||||
/* Determine the range to cut */
|
||||
|
||||
let precisionStart = (countingMode === DECIMAL_PLACES) ? afterDot // 0.(0)001234567
|
||||
: digitsStart // 0.00(1)234567
|
||||
, precisionEnd = precisionStart +
|
||||
numPrecisionDigits
|
||||
|
||||
/* Reset the last significant digit index, as it will change during the rounding/truncation. */
|
||||
|
||||
digitsEnd = -1
|
||||
|
||||
/* Perform rounding/truncation per digit, from digitsEnd to digitsStart, by using the following
|
||||
algorithm (rounding 999 → 1000, as an example):
|
||||
|
||||
step = i=3 i=2 i=1 i=0
|
||||
|
||||
chars = 0999 0999 0900 1000
|
||||
memo = ---0 --1- -1-- 0--- */
|
||||
|
||||
let allZeros = true;
|
||||
let signNeeded = isNegative;
|
||||
|
||||
for (let i = chars.length - 1, memo = 0; i >= 0; i--) {
|
||||
|
||||
let c = chars[i]
|
||||
|
||||
if (i !== 0) {
|
||||
c += memo
|
||||
|
||||
if (i >= (precisionStart + numPrecisionDigits)) {
|
||||
|
||||
const ceil = (roundingMode === ROUND) &&
|
||||
(c >= FIVE) &&
|
||||
!((c === FIVE) && memo) // prevents rounding of 1.45 to 2
|
||||
|
||||
c = ceil ? (NINE + 1) : ZERO
|
||||
}
|
||||
if (c > NINE) { c = ZERO; memo = 1; }
|
||||
else memo = 0
|
||||
|
||||
} else if (memo) c = ONE // leading extra digit (0900 → 1000)
|
||||
|
||||
chars[i] = c
|
||||
|
||||
if (c !== ZERO) {
|
||||
allZeros = false
|
||||
digitsStart = i
|
||||
digitsEnd = (digitsEnd < 0) ? (i + 1) : digitsEnd
|
||||
}
|
||||
}
|
||||
|
||||
/* Update the precision range, as `digitsStart` may have changed... & the need for a negative sign if it is only 0 */
|
||||
|
||||
if (countingMode === SIGNIFICANT_DIGITS) {
|
||||
precisionStart = digitsStart
|
||||
precisionEnd = precisionStart + numPrecisionDigits
|
||||
}
|
||||
|
||||
if (allZeros) {
|
||||
signNeeded = false
|
||||
}
|
||||
|
||||
/* Determine the input character range */
|
||||
|
||||
const readStart = ((digitsStart >= afterDot) || allZeros) ? (afterDot - 1) : digitsStart // 0.000(1)234 ----> (0).0001234
|
||||
, readEnd = (digitsEnd < afterDot) ? (afterDot ) : digitsEnd // 12(3)000 ----> 123000( )
|
||||
|
||||
/* Compute various sub-ranges */
|
||||
|
||||
const nSign = (signNeeded ? 1 : 0) // (-)123.456
|
||||
, nBeforeDot = (nSign + (afterDot - readStart)) // (-123).456
|
||||
, nAfterDot = Math.max (readEnd - afterDot, 0) // -123.(456)
|
||||
, actualLength = (readEnd - readStart) // -(123.456)
|
||||
, desiredLength = (paddingMode === NO_PADDING)
|
||||
? (actualLength) // -(123.456)
|
||||
: (precisionEnd - readStart) // -(123.456 )
|
||||
|
||||
, pad = Math.max (desiredLength - actualLength, 0) // -123.456( )
|
||||
, padStart = (nBeforeDot + 1 + nAfterDot) // -123.456( )
|
||||
, padEnd = (padStart + pad) // -123.456 ( )
|
||||
, isInteger = (nAfterDot + pad) === 0 // -123
|
||||
|
||||
/* Fill the output buffer with characters */
|
||||
|
||||
const out = new Uint8Array (nBeforeDot + (isInteger ? 0 : 1) + nAfterDot + pad)
|
||||
// ------------------------------------------------------------------------------------------ // ---------------------
|
||||
if (signNeeded) out[0] = MINUS // - minus sign
|
||||
for (i = nSign, j = readStart; i < nBeforeDot; i++, j++) out[i] = chars[j] // 123 before dot
|
||||
if (!isInteger) out[nBeforeDot] = DOT // . dot
|
||||
for (i = nBeforeDot + 1, j = afterDot; i < padStart; i++, j++) out[i] = chars[j] // 456 after dot
|
||||
for (i = padStart; i < padEnd; i++) out[i] = ZERO // 000 padding
|
||||
|
||||
/* Build a string from the output buffer */
|
||||
|
||||
return String.fromCharCode (...out)
|
||||
}
|
||||
|
||||
// toWei / fromWei
|
||||
|
||||
function fromWei (amount, decimals = 18) {
|
||||
if (amount === undefined) {
|
||||
return amount
|
||||
}
|
||||
const exponential = Math.floor (amount).toExponential () // wei must be whole numbers
|
||||
const [ n, exponent ] = exponential.split ('e')
|
||||
const newExponent = parseInt (exponent) - decimals
|
||||
return parseFloat (n + 'e' + newExponent)
|
||||
}
|
||||
|
||||
function toWei (amount, decimals = 18) {
|
||||
if (amount === undefined) {
|
||||
return amount
|
||||
}
|
||||
const exponential = parseFloat (amount).toExponential ()
|
||||
const [ n, exponent ] = exponential.split ('e')
|
||||
const newExponent = parseInt (exponent) + decimals
|
||||
return numberToString (Math.floor (parseFloat (n + 'e' + newExponent))) // wei must be whole numbers
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
module.exports = {
|
||||
toWei,
|
||||
fromWei,
|
||||
numberToString,
|
||||
precisionFromString,
|
||||
decimalToPrecision,
|
||||
truncate_to_string,
|
||||
truncate,
|
||||
precisionConstants,
|
||||
ROUND,
|
||||
TRUNCATE,
|
||||
ROUND_UP,
|
||||
ROUND_DOWN,
|
||||
DECIMAL_PLACES,
|
||||
SIGNIFICANT_DIGITS,
|
||||
TICK_SIZE,
|
||||
NO_PADDING,
|
||||
PAD_WITH_ZERO,
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
'use strict';
|
||||
|
||||
const updateCachedOrders = function (openOrders, symbol) {
|
||||
}
|
||||
|
||||
const updateOrders = function (oldOrders, newOrders, closeOpenOrders = false) {
|
||||
|
||||
for (let i = 0; i < newOrders.length; i++) {
|
||||
let newOrder = newOrders[i];
|
||||
}
|
||||
|
||||
// update local cache with open orders
|
||||
for (let j = 0; j < openOrders.length; j++) {
|
||||
const id = openOrders[j]['id'];
|
||||
this.orders[id] = openOrders[j];
|
||||
}
|
||||
let openOrdersIndexedById = this.indexBy (openOrders, 'id');
|
||||
let cachedOrderIds = Object.keys (this.orders);
|
||||
let result = [];
|
||||
for (let k = 0; k < cachedOrderIds.length; k++) {
|
||||
// match each cached order to an order in the open orders array
|
||||
// possible reasons why a cached order may be missing in the open orders array:
|
||||
// - order was closed or canceled -> update cache
|
||||
// - symbol mismatch (e.g. cached BTC/USDT, fetched ETH/USDT) -> skip
|
||||
let id = cachedOrderIds[k];
|
||||
let order = this.orders[id];
|
||||
result.push (order);
|
||||
if (!(id in openOrdersIndexedById)) {
|
||||
// cached order is not in open orders array
|
||||
// if we fetched orders by symbol and it doesn't match the cached order -> won't update the cached order
|
||||
if (symbol !== undefined && symbol !== order['symbol'])
|
||||
continue;
|
||||
// order is cached but not present in the list of open orders -> mark the cached order as closed
|
||||
if (order['status'] === 'open') {
|
||||
order = this.extend (order, {
|
||||
'status': 'closed', // likewise it might have been canceled externally (unnoticed by "us")
|
||||
'cost': undefined,
|
||||
'filled': order['amount'],
|
||||
'remaining': 0.0,
|
||||
});
|
||||
if (order['cost'] === undefined) {
|
||||
if (order['filled'] !== undefined)
|
||||
order['cost'] = order['filled'] * order['price'];
|
||||
}
|
||||
this.orders[id] = order;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
||||
console.log (this.id, orders, openOrders, closedOrders, canceledOrders)
|
||||
process.exit ()
|
||||
}
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
|
||||
updateOrders,
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
"use strict";
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// There's been a lot of messing with this code...
|
||||
// The problem is to satisfy the following requirements:
|
||||
// - properly detect isNode == true on server side and isNode == false in the browser (on client side)
|
||||
// - make sure create-react-app, react-starter-kit and other react frameworks work
|
||||
// - make sure it does not break the browserified version (when linked into a html from a cdn)
|
||||
// - make sure it does not break the webpacking and babel-transpiled scripts
|
||||
// - make sure it works in Electron
|
||||
// - make sure it works with Angular.js
|
||||
// - make sure it does not break other possible usage scenarios
|
||||
|
||||
const isBrowser = typeof window !== 'undefined'
|
||||
|
||||
const isElectron = typeof process !== 'undefined' &&
|
||||
typeof process.versions !== 'undefined' &&
|
||||
typeof process.versions.electron !== 'undefined'
|
||||
|
||||
const isWebWorker = typeof WorkerGlobalScope !== 'undefined' && (self instanceof WorkerGlobalScope)
|
||||
|
||||
const isWindows = typeof process !== 'undefined' && process.platform === "win32"
|
||||
|
||||
const isNode = !(isBrowser || isWebWorker)
|
||||
|
||||
const defaultFetch = typeof (fetch) === "undefined" ? require ('../../static_dependencies/fetch-ponyfill/fetch-node') ().fetch : fetch
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
|
||||
isBrowser,
|
||||
isElectron,
|
||||
isWebWorker,
|
||||
isNode,
|
||||
isWindows,
|
||||
defaultFetch,
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
"use strict";
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
const uuid = a => a ? (a ^ Math.random () * 16 >> a / 4).toString (16)
|
||||
: ([1e7]+-1e3+-4e3+-8e3+-1e11).replace (/[018]/g, uuid)
|
||||
|
||||
module.exports =
|
||||
|
||||
{ uuid
|
||||
|
||||
// hasFetchOHLCV → has_fetch_ohlcv; parseHTTPResponse → parse_http_response
|
||||
, unCamelCase: s => s.match (/^[A-Z0-9_]+$/) ? s : (s.replace (/[a-z0-9][A-Z]/g, x => x[0] + '_' + x[1]).replace(/[A-Z0-9][A-Z0-9][a-z]/g, x => x[0] + '_' + x[1] + x[2]).toLowerCase ())
|
||||
|
||||
, capitalize: s => s.length
|
||||
? (s.charAt (0).toUpperCase () + s.slice (1))
|
||||
: s
|
||||
, strip: s => s.replace(/^\s+|\s+$/g, '')
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
"use strict";
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
const { sleep
|
||||
, now } = require ('./time')
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
module.exports = {
|
||||
|
||||
throttle: function throttle (cfg) {
|
||||
|
||||
let lastTimestamp = now ()
|
||||
, numTokens = (cfg.numTokens !== undefined) ? cfg.numTokens : cfg.capacity
|
||||
, running = false
|
||||
, counter = 0
|
||||
|
||||
const queue = []
|
||||
|
||||
return Object.assign ((rateLimit, cost) => {
|
||||
|
||||
if (queue.length > cfg.maxCapacity)
|
||||
throw new Error ('Backlog is over max capacity of ' + cfg.maxCapacity)
|
||||
|
||||
return new Promise (async (resolve, reject) => {
|
||||
|
||||
try {
|
||||
queue.push ({ cost, resolve, reject })
|
||||
|
||||
if (!running) {
|
||||
running = true
|
||||
while (queue.length > 0) {
|
||||
const hasEnoughTokens = cfg.capacity ? (numTokens > 0) : (numTokens >= 0)
|
||||
if (hasEnoughTokens) {
|
||||
if (queue.length > 0) {
|
||||
let { cost, resolve, reject } = queue[0]
|
||||
cost = (cost || cfg.defaultCost)
|
||||
if (numTokens >= Math.min (cost, cfg.capacity)) {
|
||||
numTokens -= cost
|
||||
queue.shift ()
|
||||
resolve ()
|
||||
}
|
||||
}
|
||||
}
|
||||
const t = now ()
|
||||
, elapsed = t - lastTimestamp
|
||||
lastTimestamp = t
|
||||
numTokens = Math.min (cfg.capacity, numTokens + elapsed * (1 / rateLimit))
|
||||
await sleep (cfg.delay)
|
||||
}
|
||||
running = false
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
reject (e)
|
||||
}
|
||||
})
|
||||
|
||||
}, cfg, { configure: newCfg => throttle (Object.assign ({}, cfg, newCfg)) })
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
'use strict';
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
const now = Date.now // TODO: figure out how to utilize performance.now () properly – it's not as easy as it does not return a unix timestamp...
|
||||
const microseconds = () => now () * 1000 // TODO: utilize performance.now for that purpose
|
||||
const milliseconds = now
|
||||
const seconds = () => Math.floor (now () / 1000)
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
const setTimeout_original = setTimeout
|
||||
const setTimeout_safe = (done, ms, setTimeout = setTimeout_original /* overrideable for mocking purposes */, targetTime = now () + ms) => {
|
||||
|
||||
/* The built-in setTimeout function can fire its callback earlier than specified, so we
|
||||
need to ensure that it does not happen: sleep recursively until `targetTime` is reached... */
|
||||
|
||||
let clearInnerTimeout = () => {}
|
||||
let active = true
|
||||
|
||||
const id = setTimeout (() => {
|
||||
active = true
|
||||
const rest = targetTime - now ()
|
||||
if (rest > 0) {
|
||||
clearInnerTimeout = setTimeout_safe (done, rest, setTimeout, targetTime) // try sleep more
|
||||
} else {
|
||||
done ()
|
||||
}
|
||||
}, ms)
|
||||
|
||||
return function clear () {
|
||||
if (active) {
|
||||
active = false // dunno if IDs are unique on various platforms, so it's better to rely on this flag to exclude the possible cancellation of the wrong timer (if called after completion)
|
||||
clearTimeout (id)
|
||||
}
|
||||
clearInnerTimeout ()
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
class TimedOut extends Error {
|
||||
|
||||
constructor () {
|
||||
const message = 'timed out'
|
||||
super (message)
|
||||
this.constructor = TimedOut
|
||||
this.__proto__ = TimedOut.prototype
|
||||
this.message = message
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
const iso8601 = (timestamp) => {
|
||||
let _timestampNumber = undefined;
|
||||
if (typeof timestamp === 'number') {
|
||||
_timestampNumber = Math.floor (timestamp);
|
||||
} else {
|
||||
_timestampNumber = parseInt (timestamp, 10);
|
||||
}
|
||||
|
||||
// undefined, null and lots of nasty non-numeric values yield NaN
|
||||
if (Number.isNaN (_timestampNumber) || _timestampNumber < 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// last line of defence
|
||||
try {
|
||||
return new Date (_timestampNumber).toISOString ();
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const parse8601 = (x) => {
|
||||
if (typeof x !== 'string' || !x) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (x.match (/^[0-9]+$/)) {
|
||||
// a valid number in a string, not a date.
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (x.indexOf ('-') < 0 || x.indexOf (':') < 0) { // no date can be without a dash and a colon
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// last line of defence
|
||||
try {
|
||||
const candidate = Date.parse (((x.indexOf ('+') >= 0) || (x.slice (-1) === 'Z')) ? x : (x + 'Z').replace (/\s(\d\d):/, 'T$1:'));
|
||||
if (Number.isNaN (candidate)) {
|
||||
return undefined;
|
||||
}
|
||||
return candidate;
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const parseDate = (x) => {
|
||||
if (typeof x !== 'string' || !x) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (x.indexOf ('GMT') >= 0) {
|
||||
try {
|
||||
return Date.parse (x);
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return parse8601 (x);
|
||||
}
|
||||
|
||||
const mdy = (timestamp, infix = '-') => {
|
||||
infix = infix || ''
|
||||
const date = new Date (timestamp)
|
||||
const Y = date.getUTCFullYear ().toString ()
|
||||
let m = date.getUTCMonth () + 1
|
||||
let d = date.getUTCDate ()
|
||||
m = m < 10 ? ('0' + m) : m.toString ()
|
||||
d = d < 10 ? ('0' + d) : d.toString ()
|
||||
return m + infix + d + infix + Y
|
||||
}
|
||||
|
||||
const ymd = (timestamp, infix = '-') => {
|
||||
infix = infix || ''
|
||||
const date = new Date (timestamp)
|
||||
const Y = date.getUTCFullYear ().toString ()
|
||||
let m = date.getUTCMonth () + 1
|
||||
let d = date.getUTCDate ()
|
||||
m = m < 10 ? ('0' + m) : m.toString ()
|
||||
d = d < 10 ? ('0' + d) : d.toString ()
|
||||
return Y + infix + m + infix + d
|
||||
}
|
||||
|
||||
const ymdhms = (timestamp, infix = ' ') => {
|
||||
const date = new Date (timestamp)
|
||||
const Y = date.getUTCFullYear ()
|
||||
let m = date.getUTCMonth () + 1
|
||||
let d = date.getUTCDate ()
|
||||
let H = date.getUTCHours ()
|
||||
let M = date.getUTCMinutes ()
|
||||
let S = date.getUTCSeconds ()
|
||||
m = m < 10 ? ('0' + m) : m
|
||||
d = d < 10 ? ('0' + d) : d
|
||||
H = H < 10 ? ('0' + H) : H
|
||||
M = M < 10 ? ('0' + M) : M
|
||||
S = S < 10 ? ('0' + S) : S
|
||||
return Y + '-' + m + '-' + d + infix + H + ':' + M + ':' + S
|
||||
}
|
||||
|
||||
module.exports =
|
||||
|
||||
{
|
||||
now
|
||||
, microseconds
|
||||
, milliseconds
|
||||
, seconds
|
||||
, iso8601
|
||||
, parse8601
|
||||
, parseDate
|
||||
, mdy
|
||||
, ymd
|
||||
, ymdhms
|
||||
, setTimeout_safe
|
||||
, sleep: ms => new Promise (resolve => setTimeout_safe (resolve, ms))
|
||||
, TimedOut
|
||||
, timeout: async (ms, promise) => {
|
||||
|
||||
let clear = () => {}
|
||||
const expires = new Promise (resolve => (clear = setTimeout_safe (resolve, ms)))
|
||||
|
||||
try {
|
||||
return await Promise.race ([promise, expires.then (() => { throw new TimedOut () })])
|
||||
} finally {
|
||||
clear () // fixes https://github.com/ccxt/ccxt/issues/749
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
"use strict";
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
const isNumber = Number.isFinite
|
||||
, isInteger = Number.isInteger
|
||||
, isArray = Array.isArray
|
||||
, hasProps = o => ((o !== undefined) && (o !== null))
|
||||
, isString = s => (typeof s === 'string')
|
||||
, isObject = o => ((o !== null) && (typeof o === 'object'))
|
||||
, isDictionary = o => (isObject (o) && !isArray (o))
|
||||
, isStringCoercible = x => ((hasProps (x) && x.toString) || isNumber (x))
|
||||
|
||||
/* ............................................. */
|
||||
|
||||
const prop = (o, k) => (isObject (o) ? o[k] : undefined)
|
||||
, prop2 = (o, k1, k2) => (!isObject (o) ? undefined : (((k1 in o) && (o[k1] !== null)) ? o[k1] : o[k2]))
|
||||
|
||||
/* ............................................. */
|
||||
|
||||
const asFloat = x => ((isNumber (x) || isString (x)) ? parseFloat (x) : NaN)
|
||||
, asInteger = x => ((isNumber (x) || isString (x)) ? parseInt (x, 10) : NaN)
|
||||
|
||||
/* ............................................. */
|
||||
|
||||
module.exports = {
|
||||
|
||||
isNumber
|
||||
, isInteger
|
||||
, isArray
|
||||
, isObject
|
||||
, isString
|
||||
, isStringCoercible
|
||||
, isDictionary
|
||||
|
||||
, hasProps
|
||||
, prop
|
||||
|
||||
, asFloat
|
||||
, asInteger
|
||||
|
||||
, safeFloat: (o, k, $default, n = asFloat (prop (o, k))) => (isNumber (n) ? n : $default)
|
||||
, safeInteger: (o, k, $default, n = asInteger (prop (o, k))) => (isNumber (n) ? n : $default)
|
||||
, safeIntegerProduct: (o, k, $factor, $default, n = asInteger (prop (o, k))) => (isNumber (n) ? parseInt (n * $factor) : $default)
|
||||
, safeTimestamp: (o, k, $default, n = asFloat (prop (o, k))) => (isNumber (n) ? parseInt (n * 1000) : $default)
|
||||
, safeValue: (o, k, $default, x = prop (o, k)) => (hasProps (x) ? x : $default)
|
||||
, safeString: (o, k, $default, x = prop (o, k)) => (isStringCoercible (x) ? String (x) : $default)
|
||||
, safeStringLower: (o, k, $default, x = prop (o, k)) => (isStringCoercible (x) ? String (x).toLowerCase () : $default)
|
||||
, safeStringUpper: (o, k, $default, x = prop (o, k)) => (isStringCoercible (x) ? String (x).toUpperCase () : $default)
|
||||
|
||||
// not using safeFloats with an array argument as we're trying to save some cycles here
|
||||
// we're not using safeFloat3 either because those cases are too rare to deserve their own optimization
|
||||
|
||||
, safeFloat2: (o, k1, k2, $default, n = asFloat (prop2 (o, k1, k2))) => (isNumber (n) ? n : $default)
|
||||
, safeInteger2: (o, k1, k2, $default, n = asInteger (prop2 (o, k1, k2))) => (isNumber (n) ? n : $default)
|
||||
, safeIntegerProduct2: (o, k1, k2, $factor, $default, n = asInteger (prop2 (o, k1, k2))) => (isNumber (n) ? parseInt (n * $factor) : $default)
|
||||
, safeTimestamp2: (o, k1, k2, $default, n = asFloat (prop2 (o, k1, k2))) => (isNumber (n) ? parseInt (n * 1000) : $default)
|
||||
, safeValue2: (o, k1, k2, $default, x = prop2 (o, k1, k2)) => (hasProps (x) ? x : $default)
|
||||
, safeString2: (o, k1, k2, $default, x = prop2 (o, k1, k2)) => (isStringCoercible (x) ? String (x) : $default)
|
||||
, safeStringLower2: (o, k1, k2, $default, x = prop2 (o, k1, k2)) => (isStringCoercible (x) ? String (x).toLowerCase () : $default)
|
||||
, safeStringUpper2: (o, k1, k2, $default, x = prop2 (o, k1, k2)) => (isStringCoercible (x) ? String (x).toUpperCase () : $default)
|
||||
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
+679
@@ -0,0 +1,679 @@
|
||||
'use strict';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const Exchange = require ('./base/Exchange');
|
||||
const { ExchangeError, ArgumentsRequired, AuthenticationError, InsufficientFunds, InvalidOrder, OrderNotFound } = require ('./base/errors');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
module.exports = class bcex extends Exchange {
|
||||
describe () {
|
||||
return this.deepExtend (super.describe (), {
|
||||
'id': 'bcex',
|
||||
'name': 'BCEX',
|
||||
'countries': [ 'CN', 'CA' ],
|
||||
'version': '1',
|
||||
'has': {
|
||||
'fetchBalance': true,
|
||||
'fetchMarkets': true,
|
||||
'createOrder': true,
|
||||
'cancelOrder': true,
|
||||
'fetchTicker': true,
|
||||
'fetchTickers': false,
|
||||
'fetchTrades': true,
|
||||
'fetchOrder': true,
|
||||
'fetchOrders': true,
|
||||
'fetchClosedOrders': 'emulated',
|
||||
'fetchOpenOrders': true,
|
||||
'fetchTradingLimits': true,
|
||||
},
|
||||
'urls': {
|
||||
'logo': 'https://user-images.githubusercontent.com/51840849/77231516-851c6900-6bac-11ea-8fd6-ee5c23eddbd4.jpg',
|
||||
'api': 'https://www.bcex.top',
|
||||
'www': 'https://www.bcex.top',
|
||||
'doc': 'https://github.com/BCEX-TECHNOLOGY-LIMITED/API_Docs/wiki/Interface',
|
||||
'fees': 'https://bcex.udesk.cn/hc/articles/57085',
|
||||
'referral': 'https://www.bcex.top/register?invite_code=758978&lang=en',
|
||||
},
|
||||
'status': {
|
||||
'status': 'error',
|
||||
'updated': undefined,
|
||||
'eta': undefined,
|
||||
'url': undefined,
|
||||
},
|
||||
'api': {
|
||||
'public': {
|
||||
'get': [
|
||||
'Api_Market/getPriceList', // tickers
|
||||
'Api_Order/ticker', // last ohlcv candle (ticker)
|
||||
'Api_Order/depth', // orderbook
|
||||
'Api_Market/getCoinTrade', // ticker
|
||||
'Api_Order/marketOrder', // trades...
|
||||
],
|
||||
'post': [
|
||||
'Api_Market/getPriceList', // tickers
|
||||
'Api_Order/ticker', // last ohlcv candle (ticker)
|
||||
'Api_Order/depth', // orderbook
|
||||
'Api_Market/getCoinTrade', // ticker
|
||||
'Api_Order/marketOrder', // trades...
|
||||
],
|
||||
},
|
||||
'private': {
|
||||
'post': [
|
||||
'Api_Order/cancel',
|
||||
'Api_Order/coinTrust', // limit order
|
||||
'Api_Order/orderList', // open / all orders (my trades?)
|
||||
'Api_Order/orderInfo',
|
||||
'Api_Order/tradeList', // open / all orders
|
||||
'Api_Order/trustList', // ?
|
||||
'Api_User/userBalance',
|
||||
],
|
||||
},
|
||||
},
|
||||
'fees': {
|
||||
'trading': {
|
||||
'tierBased': false,
|
||||
'percentage': true,
|
||||
'maker': 0.1 / 100,
|
||||
'taker': 0.2 / 100,
|
||||
},
|
||||
'funding': {
|
||||
'tierBased': false,
|
||||
'percentage': false,
|
||||
'withdraw': {
|
||||
'ckusd': 0.0,
|
||||
'other': 0.05 / 100,
|
||||
},
|
||||
'deposit': {},
|
||||
},
|
||||
},
|
||||
'exceptions': {
|
||||
'该币不存在,非法操作': ExchangeError, // { code: 1, msg: "该币不存在,非法操作" } - returned when a required symbol parameter is missing in the request (also, maybe on other types of errors as well)
|
||||
'公钥不合法': AuthenticationError, // { code: 1, msg: '公钥不合法' } - wrong public key
|
||||
'您的可用余额不足': InsufficientFunds, // { code: 1, msg: '您的可用余额不足' } - your available balance is insufficient
|
||||
'您的btc不足': InsufficientFunds, // { code: 1, msg: '您的btc不足' } - your btc is insufficient
|
||||
'参数非法': InvalidOrder, // {'code': 1, 'msg': '参数非法'} - 'Parameter illegal'
|
||||
'订单信息不存在': OrderNotFound, // {'code': 1, 'msg': '订单信息不存在'} - 'Order information does not exist'
|
||||
},
|
||||
'options': {
|
||||
'limits': {
|
||||
// hardcoding is deprecated, using these predefined values is not recommended, use loadTradingLimits instead
|
||||
'AFC/CKUSD': { 'precision': { 'amount': 2, 'price': 4 }, 'limits': { 'amount': { 'min': 6, 'max': 120000 }}},
|
||||
'AFC/ETH': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 6, 'max': 120000 }}},
|
||||
'AFT/ETH': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 15, 'max': 300000 }}},
|
||||
'AICC/CNET': { 'precision': { 'amount': 2, 'price': 2 }, 'limits': { 'amount': { 'min': 5, 'max': 50000 }}},
|
||||
'AIDOC/CKUSD': { 'precision': { 'amount': 2, 'price': 4 }, 'limits': { 'amount': { 'min': 5, 'max': 100000 }}},
|
||||
'AISI/ETH': { 'precision': { 'amount': 4, 'price': 2 }, 'limits': { 'amount': { 'min': 0.001, 'max': 500 }}},
|
||||
'AIT/ETH': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 20, 'max': 400000 }}},
|
||||
'ANS/BTC': { 'precision': { 'amount': 4, 'price': 8 }, 'limits': { 'amount': { 'min': 0.1, 'max': 500 }}},
|
||||
'ANS/CKUSD': { 'precision': { 'amount': 2, 'price': 2 }, 'limits': { 'amount': { 'min': 0.1, 'max': 1000 }}},
|
||||
'ARC/CNET': { 'precision': { 'amount': 2, 'price': 4 }, 'limits': { 'amount': { 'min': 60, 'max': 600000 }}},
|
||||
'AXF/CNET': { 'precision': { 'amount': 2, 'price': 4 }, 'limits': { 'amount': { 'min': 100, 'max': 1000000 }}},
|
||||
'BASH/CKUSD': { 'precision': { 'amount': 2, 'price': 4 }, 'limits': { 'amount': { 'min': 250, 'max': 3000000 }}},
|
||||
'BATT/ETH': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 60, 'max': 1500000 }}},
|
||||
'BCD/BTC': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 0.3, 'max': 7000 }}},
|
||||
'BHPC/BTC': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 2, 'max': 70000 }}},
|
||||
'BHPC/ETH': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 2, 'max': 60000 }}},
|
||||
'BOPO/BTC': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 100, 'max': 2000000 }}},
|
||||
'BOPO/CKUSD': { 'precision': { 'amount': 2, 'price': 4 }, 'limits': { 'amount': { 'min': 100, 'max': 10000000 }}},
|
||||
'BTC/CKUSD': { 'precision': { 'amount': 4, 'price': 2 }, 'limits': { 'amount': { 'min': 0.001, 'max': 10 }}},
|
||||
'BTC/CNET': { 'precision': { 'amount': 4, 'price': 2 }, 'limits': { 'amount': { 'min': 0.0005, 'max': 5 }}},
|
||||
'BTC/USDT': { 'precision': { 'amount': 4, 'price': 2 }, 'limits': { 'amount': { 'min': 0.0002, 'max': 4 }}},
|
||||
'BTE/CNET': { 'precision': { 'amount': 2, 'price': 4 }, 'limits': { 'amount': { 'min': 25, 'max': 250000 }}},
|
||||
'BU/ETH': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 20, 'max': 400000 }}},
|
||||
'CIC/CNET': { 'precision': { 'amount': 2, 'price': 4 }, 'limits': { 'amount': { 'min': 3000, 'max': 30000000 }}},
|
||||
'CIT/CKUSD': { 'precision': { 'amount': 2, 'price': 4 }, 'limits': { 'amount': { 'min': 4, 'max': 40000 }}},
|
||||
'CIT/ETH': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 4, 'max': 40000 }}},
|
||||
'CMT/CKUSD': { 'precision': { 'amount': 2, 'price': 4 }, 'limits': { 'amount': { 'min': 5, 'max': 2500000 }}},
|
||||
'CNET/CKUSD': { 'precision': { 'amount': 2, 'price': 4 }, 'limits': { 'amount': { 'min': 12, 'max': 120000 }}},
|
||||
'CNMC/BTC': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 4, 'max': 50000 }}},
|
||||
'CTC/ETH': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 5, 'max': 550000 }}},
|
||||
'CZR/ETH': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 12, 'max': 500000 }}},
|
||||
'DCON/ETH': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 8, 'max': 300000 }}},
|
||||
'DCT/BTC': { 'precision': { 'amount': 4, 'price': 8 }, 'limits': { 'amount': { 'min': 2, 'max': 40000 }}},
|
||||
'DCT/CKUSD': { 'precision': { 'amount': 2, 'price': 3 }, 'limits': { 'amount': { 'min': 2, 'max': 2000 }}},
|
||||
'DOGE/BTC': { 'precision': { 'amount': 4, 'price': 8 }, 'limits': { 'amount': { 'min': 3000, 'max': 14000000 }}},
|
||||
'DOGE/CKUSD': { 'precision': { 'amount': 2, 'price': 6 }, 'limits': { 'amount': { 'min': 500, 'max': 2000000 }}},
|
||||
'DRCT/ETH': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 16, 'max': 190000 }}},
|
||||
'ELA/BTC': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 0.02, 'max': 500 }}},
|
||||
'ELF/BTC': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 0.1, 'max': 100000 }}},
|
||||
'ELF/CKUSD': { 'precision': { 'amount': 2, 'price': 3 }, 'limits': { 'amount': { 'min': 0.01, 'max': 100000 }}},
|
||||
'EOS/CKUSD': { 'precision': { 'amount': 2, 'price': 2 }, 'limits': { 'amount': { 'min': 0.5, 'max': 5000 }}},
|
||||
'EOS/CNET': { 'precision': { 'amount': 2, 'price': 2 }, 'limits': { 'amount': { 'min': 2.5, 'max': 30000 }}},
|
||||
'EOS/ETH': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 0.18, 'max': 1800 }}},
|
||||
'ETC/BTC': { 'precision': { 'amount': 4, 'price': 8 }, 'limits': { 'amount': { 'min': 0.2, 'max': 2500 }}},
|
||||
'ETC/CKUSD': { 'precision': { 'amount': 2, 'price': 2 }, 'limits': { 'amount': { 'min': 0.2, 'max': 2500 }}},
|
||||
'ETF/ETH': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 7, 'max': 150000 }}},
|
||||
'ETH/BTC': { 'precision': { 'amount': 4, 'price': 8 }, 'limits': { 'amount': { 'min': 0.015, 'max': 100 }}},
|
||||
'ETH/CKUSD': { 'precision': { 'amount': 4, 'price': 4 }, 'limits': { 'amount': { 'min': 0.005, 'max': 100 }}},
|
||||
'ETH/USDT': { 'precision': { 'amount': 4, 'price': 2 }, 'limits': { 'amount': { 'min': 0.005, 'max': 100 }}},
|
||||
'FCT/BTC': { 'precision': { 'amount': 4, 'price': 8 }, 'limits': { 'amount': { 'min': 0.24, 'max': 1000 }}},
|
||||
'FCT/CKUSD': { 'precision': { 'amount': 2, 'price': 2 }, 'limits': { 'amount': { 'min': 0.24, 'max': 1000 }}},
|
||||
'GAME/CNET': { 'precision': { 'amount': 2, 'price': 2 }, 'limits': { 'amount': { 'min': 1, 'max': 10000 }}},
|
||||
'GOOC/CKUSD': { 'precision': { 'amount': 2, 'price': 4 }, 'limits': { 'amount': { 'min': 200, 'max': 2000000 }}},
|
||||
'GP/CNET': { 'precision': { 'amount': 2, 'price': 4 }, 'limits': { 'amount': { 'min': 600, 'max': 6000000 }}},
|
||||
'HSC/ETH': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 1000, 'max': 20000000 }}},
|
||||
'IFISH/ETH': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 300, 'max': 8000000 }}},
|
||||
'IIC/ETH': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 50, 'max': 4000000 }}},
|
||||
'IMOS/ETH': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 15, 'max': 300000 }}},
|
||||
'JC/CNET': { 'precision': { 'amount': 2, 'price': 4 }, 'limits': { 'amount': { 'min': 300, 'max': 3000000 }}},
|
||||
'LBTC/BTC': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 0.1, 'max': 3000 }}},
|
||||
'LEC/CNET': { 'precision': { 'amount': 2, 'price': 4 }, 'limits': { 'amount': { 'min': 500, 'max': 5000000 }}},
|
||||
'LKY/CKUSD': { 'precision': { 'amount': 2, 'price': 4 }, 'limits': { 'amount': { 'min': 10, 'max': 70000 }}},
|
||||
'LKY/ETH': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 10, 'max': 100000 }}},
|
||||
'LMC/CNET': { 'precision': { 'amount': 2, 'price': 4 }, 'limits': { 'amount': { 'min': 25, 'max': 250000 }}},
|
||||
'LSK/CNET': { 'precision': { 'amount': 2, 'price': 2 }, 'limits': { 'amount': { 'min': 0.3, 'max': 3000 }}},
|
||||
'LTC/BTC': { 'precision': { 'amount': 4, 'price': 8 }, 'limits': { 'amount': { 'min': 0.01, 'max': 500 }}},
|
||||
'LTC/CKUSD': { 'precision': { 'amount': 2, 'price': 2 }, 'limits': { 'amount': { 'min': 0.01, 'max': 500 }}},
|
||||
'LTC/USDT': { 'precision': { 'amount': 4, 'price': 2 }, 'limits': { 'amount': { 'min': 0.02, 'max': 450 }}},
|
||||
'MC/CNET': { 'precision': { 'amount': 2, 'price': 6 }, 'limits': { 'amount': { 'min': 10000, 'max': 100000000 }}},
|
||||
'MCC/CKUSD': { 'precision': { 'amount': 2, 'price': 4 }, 'limits': { 'amount': { 'min': 30, 'max': 350000 }}},
|
||||
'MOC/ETH': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 25, 'max': 600000 }}},
|
||||
'MRYC/CNET': { 'precision': { 'amount': 2, 'price': 4 }, 'limits': { 'amount': { 'min': 300, 'max': 3000000 }}},
|
||||
'MT/ETH': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 200, 'max': 6000000 }}},
|
||||
'MXI/CNET': { 'precision': { 'amount': 2, 'price': 6 }, 'limits': { 'amount': { 'min': 5000, 'max': 60000000 }}},
|
||||
'NAI/ETH': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 10, 'max': 100000 }}},
|
||||
'NAS/BTC': { 'precision': { 'amount': 4, 'price': 8 }, 'limits': { 'amount': { 'min': 0.2, 'max': 15000 }}},
|
||||
'NAS/CKUSD': { 'precision': { 'amount': 2, 'price': 2 }, 'limits': { 'amount': { 'min': 0.5, 'max': 5000 }}},
|
||||
'NEWOS/ETH': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 65, 'max': 700000 }}},
|
||||
'NKN/ETH': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 3, 'max': 350000 }}},
|
||||
'NTK/ETH': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 2, 'max': 30000 }}},
|
||||
'ONT/CKUSD': { 'precision': { 'amount': 2, 'price': 3 }, 'limits': { 'amount': { 'min': 0.2, 'max': 2000 }}},
|
||||
'ONT/ETH': { 'precision': { 'amount': 3, 'price': 8 }, 'limits': { 'amount': { 'min': 0.01, 'max': 1000 }}},
|
||||
'PNT/ETH': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 80, 'max': 800000 }}},
|
||||
'PST/ETH': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 5, 'max': 100000 }}},
|
||||
'PTT/ETH': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 450, 'max': 10000000 }}},
|
||||
'QTUM/BTC': { 'precision': { 'amount': 4, 'price': 8 }, 'limits': { 'amount': { 'min': 0.4, 'max': 2800 }}},
|
||||
'QTUM/CKUSD': { 'precision': { 'amount': 2, 'price': 2 }, 'limits': { 'amount': { 'min': 0.1, 'max': 1000 }}},
|
||||
'RATING/ETH': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 500, 'max': 10000000 }}},
|
||||
'RHC/CNET': { 'precision': { 'amount': 2, 'price': 4 }, 'limits': { 'amount': { 'min': 1000, 'max': 10000000 }}},
|
||||
'SDA/ETH': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 20, 'max': 500000 }}},
|
||||
'SDD/CKUSD': { 'precision': { 'amount': 2, 'price': 3 }, 'limits': { 'amount': { 'min': 10, 'max': 100000 }}},
|
||||
'SHC/CNET': { 'precision': { 'amount': 2, 'price': 4 }, 'limits': { 'amount': { 'min': 250, 'max': 2500000 }}},
|
||||
'SHE/ETH': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 100, 'max': 5000000 }}},
|
||||
'SMC/CNET': { 'precision': { 'amount': 2, 'price': 6 }, 'limits': { 'amount': { 'min': 1000, 'max': 10000000 }}},
|
||||
'SOP/ETH': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 50, 'max': 1000000 }}},
|
||||
'TAC/ETH': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 35, 'max': 800000 }}},
|
||||
'TIP/ETH': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 7, 'max': 200000 }}},
|
||||
'TKT/ETH': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 40, 'max': 400000 }}},
|
||||
'TLC/ETH': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 500, 'max': 10000000 }}},
|
||||
'TNC/ETH': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 10, 'max': 110000 }}},
|
||||
'TUB/ETH': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 200, 'max': 8000000 }}},
|
||||
'UC/ETH': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 100, 'max': 3000000 }}},
|
||||
'UDB/CNET': { 'precision': { 'amount': 2, 'price': 6 }, 'limits': { 'amount': { 'min': 2000, 'max': 40000000 }}},
|
||||
'UIC/CKUSD': { 'precision': { 'amount': 2, 'price': 4 }, 'limits': { 'amount': { 'min': 5, 'max': 150000 }}},
|
||||
'VAAC/ETH': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 10, 'max': 250000 }}},
|
||||
'VPN/CNET': { 'precision': { 'amount': 2, 'price': 4 }, 'limits': { 'amount': { 'min': 200, 'max': 2000000 }}},
|
||||
'VSC/ETH': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 30, 'max': 650000 }}},
|
||||
'WAVES/CKUSD': { 'precision': { 'amount': 2, 'price': 3 }, 'limits': { 'amount': { 'min': 0.15, 'max': 1500 }}},
|
||||
'WDNA/ETH': { 'precision': { 'amount': 2, 'price': 8 }, 'limits': { 'amount': { 'min': 100, 'max': 250000 }}},
|
||||
'WIC/CKUSD': { 'precision': { 'amount': 2, 'price': 4 }, 'limits': { 'amount': { 'min': 3, 'max': 30000 }}},
|
||||
'XAS/CNET': { 'precision': { 'amount': 2, 'price': 2 }, 'limits': { 'amount': { 'min': 2.5, 'max': 25000 }}},
|
||||
'XLM/BTC': { 'precision': { 'amount': 4, 'price': 8 }, 'limits': { 'amount': { 'min': 10, 'max': 300000 }}},
|
||||
'XLM/CKUSD': { 'precision': { 'amount': 2, 'price': 4 }, 'limits': { 'amount': { 'min': 1, 'max': 300000 }}},
|
||||
'XLM/USDT': { 'precision': { 'amount': 2, 'price': 4 }, 'limits': { 'amount': { 'min': 5, 'max': 150000 }}},
|
||||
'XRP/BTC': { 'precision': { 'amount': 4, 'price': 8 }, 'limits': { 'amount': { 'min': 24, 'max': 100000 }}},
|
||||
'XRP/CKUSD': { 'precision': { 'amount': 2, 'price': 3 }, 'limits': { 'amount': { 'min': 5, 'max': 50000 }}},
|
||||
'YBCT/BTC': { 'precision': { 'amount': 4, 'price': 8 }, 'limits': { 'amount': { 'min': 15, 'max': 200000 }}},
|
||||
'YBCT/CKUSD': { 'precision': { 'amount': 2, 'price': 4 }, 'limits': { 'amount': { 'min': 10, 'max': 200000 }}},
|
||||
'YBY/CNET': { 'precision': { 'amount': 2, 'price': 6 }, 'limits': { 'amount': { 'min': 25000, 'max': 250000000 }}},
|
||||
'ZEC/BTC': { 'precision': { 'amount': 4, 'price': 8 }, 'limits': { 'amount': { 'min': 0.02, 'max': 100 }}},
|
||||
'ZEC/CKUSD': { 'precision': { 'amount': 4, 'price': 2 }, 'limits': { 'amount': { 'min': 0.02, 'max': 100 }}},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async fetchTradingLimits (symbols = undefined, params = {}) {
|
||||
// this method should not be called directly, use loadTradingLimits () instead
|
||||
// by default it will try load withdrawal fees of all currencies (with separate requests, sequentially)
|
||||
// however if you define symbols = [ 'ETH/BTC', 'LTC/BTC' ] in args it will only load those
|
||||
await this.loadMarkets ();
|
||||
if (symbols === undefined) {
|
||||
symbols = this.symbols;
|
||||
}
|
||||
const result = {};
|
||||
for (let i = 0; i < symbols.length; i++) {
|
||||
const symbol = symbols[i];
|
||||
result[symbol] = await this.fetchTradingLimitsById (this.marketId (symbol), params);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async fetchTradingLimitsById (id, params = {}) {
|
||||
const request = {
|
||||
'symbol': id,
|
||||
};
|
||||
const response = await this.publicPostApiOrderTicker (this.extend (request, params));
|
||||
//
|
||||
// { code: 0,
|
||||
// msg: "获取牌价信息成功",
|
||||
// data: { high: 0.03721392,
|
||||
// low: 0.03335362,
|
||||
// buy: "0.03525757",
|
||||
// sell: "0.03531160",
|
||||
// last: 0.0352634,
|
||||
// vol: "184742.4176",
|
||||
// min_trade: "0.01500000",
|
||||
// max_trade: "100.00000000",
|
||||
// number_float: "4",
|
||||
// price_float: "8" } } }
|
||||
//
|
||||
return this.parseTradingLimits (this.safeValue (response, 'data', {}));
|
||||
}
|
||||
|
||||
parseTradingLimits (limits, symbol = undefined, params = {}) {
|
||||
//
|
||||
// { high: 0.03721392,
|
||||
// low: 0.03335362,
|
||||
// buy: "0.03525757",
|
||||
// sell: "0.03531160",
|
||||
// last: 0.0352634,
|
||||
// vol: "184742.4176",
|
||||
// min_trade: "0.01500000",
|
||||
// max_trade: "100.00000000",
|
||||
// number_float: "4",
|
||||
// price_float: "8" }
|
||||
//
|
||||
return {
|
||||
'info': limits,
|
||||
'precision': {
|
||||
'amount': this.safeInteger (limits, 'number_float'),
|
||||
'price': this.safeInteger (limits, 'price_float'),
|
||||
},
|
||||
'limits': {
|
||||
'amount': {
|
||||
'min': this.safeFloat (limits, 'min_trade'),
|
||||
'max': this.safeFloat (limits, 'max_trade'),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async fetchMarkets (params = {}) {
|
||||
const response = await this.publicGetApiMarketGetPriceList (params);
|
||||
const result = [];
|
||||
const keys = Object.keys (response);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const currentMarketId = keys[i];
|
||||
const currentMarkets = response[currentMarketId];
|
||||
for (let j = 0; j < currentMarkets.length; j++) {
|
||||
const market = currentMarkets[j];
|
||||
const baseId = this.safeString (market, 'coin_from');
|
||||
const quoteId = this.safeString (market, 'coin_to');
|
||||
let base = baseId.toUpperCase ();
|
||||
let quote = quoteId.toUpperCase ();
|
||||
base = this.safeCurrencyCode (base);
|
||||
quote = this.safeCurrencyCode (quote);
|
||||
const id = baseId + '2' + quoteId;
|
||||
const symbol = base + '/' + quote;
|
||||
const active = true;
|
||||
const defaults = this.safeValue (this.options['limits'], symbol, {});
|
||||
result.push (this.extend ({
|
||||
'id': id,
|
||||
'symbol': symbol,
|
||||
'base': base,
|
||||
'quote': quote,
|
||||
'baseId': baseId,
|
||||
'quoteId': quoteId,
|
||||
'active': active,
|
||||
// overrided by defaults from this.options['limits']
|
||||
'precision': {
|
||||
'amount': undefined,
|
||||
'price': undefined,
|
||||
},
|
||||
// overrided by defaults from this.options['limits']
|
||||
'limits': {
|
||||
'amount': { 'min': undefined, 'max': undefined },
|
||||
'price': { 'min': undefined, 'max': undefined },
|
||||
'cost': { 'min': undefined, 'max': undefined },
|
||||
},
|
||||
'info': market,
|
||||
}, defaults));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
parseTrade (trade, market = undefined) {
|
||||
let symbol = undefined;
|
||||
if (market !== undefined) {
|
||||
symbol = market['symbol'];
|
||||
}
|
||||
const timestamp = this.safeTimestamp2 (trade, 'date', 'created');
|
||||
const id = this.safeString (trade, 'tid');
|
||||
const orderId = this.safeString (trade, 'order_id');
|
||||
const amount = this.safeFloat2 (trade, 'number', 'amount');
|
||||
const price = this.safeFloat (trade, 'price');
|
||||
let cost = undefined;
|
||||
if (price !== undefined) {
|
||||
if (amount !== undefined) {
|
||||
cost = amount * price;
|
||||
}
|
||||
}
|
||||
let side = this.safeString (trade, 'side');
|
||||
if (side === 'sale') {
|
||||
side = 'sell';
|
||||
}
|
||||
return {
|
||||
'info': trade,
|
||||
'id': id,
|
||||
'timestamp': timestamp,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'symbol': symbol,
|
||||
'type': undefined,
|
||||
'side': side,
|
||||
'price': price,
|
||||
'amount': amount,
|
||||
'cost': cost,
|
||||
'order': orderId,
|
||||
'fee': undefined,
|
||||
'takerOrMaker': undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async fetchTrades (symbol, since = undefined, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const request = {
|
||||
'symbol': this.marketId (symbol),
|
||||
};
|
||||
if (limit !== undefined) {
|
||||
request['limit'] = limit;
|
||||
}
|
||||
const market = this.market (symbol);
|
||||
const response = await this.publicPostApiOrderMarketOrder (this.extend (request, params));
|
||||
return this.parseTrades (response['data'], market, since, limit);
|
||||
}
|
||||
|
||||
async fetchBalance (params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const response = await this.privatePostApiUserUserBalance (params);
|
||||
const data = this.safeValue (response, 'data');
|
||||
let keys = Object.keys (data);
|
||||
const result = { };
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
const amount = this.safeFloat (data, key);
|
||||
const parts = key.split ('_');
|
||||
const currencyId = parts[0];
|
||||
const lockOrOver = parts[1];
|
||||
const code = this.safeCurrencyCode (currencyId);
|
||||
if (!(code in result)) {
|
||||
result[code] = this.account ();
|
||||
}
|
||||
if (lockOrOver === 'lock') {
|
||||
result[code]['used'] = parseFloat (amount);
|
||||
} else {
|
||||
result[code]['free'] = parseFloat (amount);
|
||||
}
|
||||
}
|
||||
keys = Object.keys (result);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
const total = this.sum (result[key]['used'], result[key]['free']);
|
||||
result[key]['total'] = total;
|
||||
}
|
||||
result['info'] = data;
|
||||
return this.parseBalance (result);
|
||||
}
|
||||
|
||||
async fetchTicker (symbol, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.markets[symbol];
|
||||
const request = {
|
||||
'part': market['quoteId'],
|
||||
'coin': market['baseId'],
|
||||
};
|
||||
const response = await this.publicPostApiMarketGetCoinTrade (this.extend (request, params));
|
||||
const timestamp = this.milliseconds ();
|
||||
return {
|
||||
'symbol': symbol,
|
||||
'timestamp': timestamp,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'high': this.safeFloat (response, 'max'),
|
||||
'low': this.safeFloat (response, 'min'),
|
||||
'bid': this.safeFloat (response, 'buy'),
|
||||
'bidVolume': undefined,
|
||||
'ask': this.safeFloat (response, 'sale'),
|
||||
'askVolume': undefined,
|
||||
'vwap': undefined,
|
||||
'open': undefined,
|
||||
'close': this.safeFloat (response, 'price'),
|
||||
'last': this.safeFloat (response, 'price'),
|
||||
'previousClose': undefined,
|
||||
'change': undefined,
|
||||
'percentage': this.safeFloat (response, 'change_24h'),
|
||||
'average': undefined,
|
||||
'baseVolume': this.safeFloat (response, 'volume_24h'),
|
||||
'quoteVolume': undefined,
|
||||
'info': response,
|
||||
};
|
||||
}
|
||||
|
||||
async fetchOrderBook (symbol, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const marketId = this.marketId (symbol);
|
||||
const request = {
|
||||
'symbol': marketId,
|
||||
};
|
||||
const response = await this.publicPostApiOrderDepth (this.extend (request, params));
|
||||
const data = this.safeValue (response, 'data');
|
||||
const timestamp = this.safeTimestamp (data, 'date');
|
||||
return this.parseOrderBook (data, timestamp);
|
||||
}
|
||||
|
||||
async fetchMyTrades (symbol = undefined, since = undefined, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const request = {
|
||||
'symbol': market['id'],
|
||||
};
|
||||
const response = await this.privatePostApiOrderOrderList (this.extend (request, params));
|
||||
return this.parseTrades (response['data'], market, since, limit);
|
||||
}
|
||||
|
||||
parseOrderStatus (status) {
|
||||
const statuses = {
|
||||
'0': 'open',
|
||||
'1': 'open', // partially filled
|
||||
'2': 'closed',
|
||||
'3': 'canceled',
|
||||
};
|
||||
return this.safeString (statuses, status, status);
|
||||
}
|
||||
|
||||
async fetchOrder (id, symbol = undefined, params = {}) {
|
||||
if (symbol === undefined) {
|
||||
throw new ArgumentsRequired (this.id + ' fetchOrder requires a `symbol` argument');
|
||||
}
|
||||
await this.loadMarkets ();
|
||||
const request = {
|
||||
'symbol': this.marketId (symbol),
|
||||
'trust_id': id,
|
||||
};
|
||||
const response = await this.privatePostApiOrderOrderInfo (this.extend (request, params));
|
||||
const order = this.safeValue (response, 'data');
|
||||
const timestamp = this.safeTimestamp (order, 'created');
|
||||
const status = this.parseOrderStatus (this.safeString (order, 'status'));
|
||||
let side = this.safeString (order, 'flag');
|
||||
if (side === 'sale') {
|
||||
side = 'sell';
|
||||
}
|
||||
// Can't use parseOrder because the data format is different btw endpoint for fetchOrder and fetchOrders
|
||||
return {
|
||||
'info': order,
|
||||
'id': id,
|
||||
'timestamp': timestamp,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'lastTradeTimestamp': undefined,
|
||||
'symbol': symbol,
|
||||
'type': undefined,
|
||||
'side': side,
|
||||
'price': this.safeFloat (order, 'price'),
|
||||
'cost': undefined,
|
||||
'average': this.safeFloat (order, 'avg_price'),
|
||||
'amount': this.safeFloat (order, 'number'),
|
||||
'filled': this.safeFloat (order, 'numberdeal'),
|
||||
'remaining': this.safeFloat (order, 'numberover'),
|
||||
'status': status,
|
||||
'fee': undefined,
|
||||
'clientOrderId': undefined,
|
||||
'trades': undefined,
|
||||
};
|
||||
}
|
||||
|
||||
parseOrder (order, market = undefined) {
|
||||
const id = this.safeString (order, 'id');
|
||||
const timestamp = this.safeTimestamp (order, 'datetime');
|
||||
let symbol = undefined;
|
||||
if (market !== undefined) {
|
||||
symbol = market['symbol'];
|
||||
}
|
||||
const type = undefined;
|
||||
let side = this.safeString (order, 'type');
|
||||
if (side === 'sale') {
|
||||
side = 'sell';
|
||||
}
|
||||
const price = this.safeFloat (order, 'price');
|
||||
const average = this.safeFloat (order, 'avg_price');
|
||||
const amount = this.safeFloat (order, 'amount');
|
||||
const remaining = this.safeFloat (order, 'amount_outstanding');
|
||||
const filled = amount - remaining;
|
||||
const status = this.parseOrderStatus (this.safeString (order, 'status'));
|
||||
const cost = filled * price;
|
||||
const fee = undefined;
|
||||
const result = {
|
||||
'info': order,
|
||||
'id': id,
|
||||
'clientOrderId': undefined,
|
||||
'timestamp': timestamp,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'lastTradeTimestamp': undefined,
|
||||
'symbol': symbol,
|
||||
'type': type,
|
||||
'side': side,
|
||||
'price': price,
|
||||
'cost': cost,
|
||||
'average': average,
|
||||
'amount': amount,
|
||||
'filled': filled,
|
||||
'remaining': remaining,
|
||||
'status': status,
|
||||
'fee': fee,
|
||||
'trades': undefined,
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
async fetchOrdersByType (type, symbol = undefined, since = undefined, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const request = {
|
||||
'type': type,
|
||||
};
|
||||
let market = undefined;
|
||||
if (symbol !== undefined) {
|
||||
market = this.market (symbol);
|
||||
request['symbol'] = market['id'];
|
||||
}
|
||||
const response = await this.privatePostApiOrderTradeList (this.extend (request, params));
|
||||
if ('data' in response) {
|
||||
return this.parseOrders (response['data'], market, since, limit);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
async fetchOpenOrders (symbol = undefined, since = undefined, limit = undefined, params = {}) {
|
||||
return this.fetchOrdersByType ('open', symbol, since, limit, params);
|
||||
}
|
||||
|
||||
async fetchClosedOrders (symbol = undefined, since = undefined, limit = undefined, params = {}) {
|
||||
const orders = await this.fetchOrders (symbol, since, limit, params);
|
||||
return this.filterBy (orders, 'status', 'closed');
|
||||
}
|
||||
|
||||
async fetchOrders (symbol = undefined, since = undefined, limit = undefined, params = {}) {
|
||||
return this.fetchOrdersByType ('all', symbol, since, limit, params);
|
||||
}
|
||||
|
||||
async createOrder (symbol, type, side, amount, price = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const request = {
|
||||
'symbol': this.marketId (symbol),
|
||||
'type': side,
|
||||
'price': this.priceToPrecision (symbol, price),
|
||||
'number': this.amountToPrecision (symbol, amount),
|
||||
};
|
||||
const response = await this.privatePostApiOrderCoinTrust (this.extend (request, params));
|
||||
const data = this.safeValue (response, 'data', {});
|
||||
const id = this.safeString (data, 'order_id');
|
||||
return {
|
||||
'info': response,
|
||||
'id': id,
|
||||
};
|
||||
}
|
||||
|
||||
async cancelOrder (id, symbol = undefined, params = {}) {
|
||||
if (symbol === undefined) {
|
||||
throw new ArgumentsRequired (this.id + ' cancelOrder requires a `symbol` argument');
|
||||
}
|
||||
await this.loadMarkets ();
|
||||
const request = {};
|
||||
if (symbol !== undefined) {
|
||||
request['symbol'] = this.marketId (symbol);
|
||||
}
|
||||
if (id !== undefined) {
|
||||
request['order_id'] = id;
|
||||
}
|
||||
return await this.privatePostApiOrderCancel (this.extend (request, params));
|
||||
}
|
||||
|
||||
sign (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) {
|
||||
let url = this.urls['api'] + '/' + this.implodeParams (path, params);
|
||||
const query = this.omit (params, this.extractParams (path));
|
||||
if (api === 'public') {
|
||||
if (Object.keys (query).length) {
|
||||
url += '?' + this.urlencode (query);
|
||||
}
|
||||
} else {
|
||||
this.checkRequiredCredentials ();
|
||||
let payload = this.urlencode ({ 'api_key': this.apiKey });
|
||||
if (Object.keys (query).length) {
|
||||
payload += '&' + this.urlencode (this.keysort (query));
|
||||
}
|
||||
const auth = payload + '&secret_key=' + this.secret;
|
||||
const signature = this.hash (this.encode (auth));
|
||||
body = payload + '&sign=' + signature;
|
||||
headers = {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
};
|
||||
}
|
||||
return { 'url': url, 'method': method, 'body': body, 'headers': headers };
|
||||
}
|
||||
|
||||
handleErrors (code, reason, url, method, headers, body, response, requestHeaders, requestBody) {
|
||||
if (response === undefined) {
|
||||
return; // fallback to default error handler
|
||||
}
|
||||
const errorCode = this.safeValue (response, 'code');
|
||||
if (errorCode !== undefined) {
|
||||
if (errorCode !== 0) {
|
||||
//
|
||||
// { code: 1, msg: "该币不存在,非法操作" } - returned when a required symbol parameter is missing in the request (also, maybe on other types of errors as well)
|
||||
// { code: 1, msg: '公钥不合法' } - wrong public key
|
||||
// { code: 1, msg: '价格输入有误,请检查你的数值精度' } - 'The price input is incorrect, please check your numerical accuracy'
|
||||
// { code: 1, msg: '单笔最小交易数量不能小于0.00100000,请您重新挂单'} -
|
||||
// 'The minimum number of single transactions cannot be less than 0.00100000. Please re-post the order'
|
||||
//
|
||||
const message = this.safeString (response, 'msg');
|
||||
const feedback = this.id + ' ' + message;
|
||||
this.throwExactlyMatchedException (this.exceptions, message, feedback);
|
||||
if (message.indexOf ('请您重新挂单') >= 0) { // minimum limit
|
||||
throw new InvalidOrder (feedback);
|
||||
} else {
|
||||
throw new ExchangeError (feedback);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
'use strict';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const hitbtc = require ('./hitbtc');
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
module.exports = class bequant extends hitbtc {
|
||||
describe () {
|
||||
return this.deepExtend (super.describe (), {
|
||||
'id': 'bequant',
|
||||
'name': 'Bequant',
|
||||
'countries': [ 'MT' ], // Malta
|
||||
'pro': true,
|
||||
'urls': {
|
||||
'logo': 'https://user-images.githubusercontent.com/1294454/55248342-a75dfe00-525a-11e9-8aa2-05e9dca943c6.jpg',
|
||||
'api': {
|
||||
'public': 'https://api.bequant.io',
|
||||
'private': 'https://api.bequant.io',
|
||||
},
|
||||
'www': 'https://bequant.io',
|
||||
'doc': [
|
||||
'https://api.bequant.io/',
|
||||
],
|
||||
'fees': [
|
||||
'https://bequant.io/fees-and-limits',
|
||||
],
|
||||
'referral': 'https://bequant.io',
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
+968
@@ -0,0 +1,968 @@
|
||||
'use strict';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const Exchange = require ('./base/Exchange');
|
||||
const { ExchangeError, AccountSuspended, ArgumentsRequired, AuthenticationError, DDoSProtection, ExchangeNotAvailable, InvalidOrder, OrderNotFound, PermissionDenied, InsufficientFunds } = require ('./base/errors');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
module.exports = class bibox extends Exchange {
|
||||
describe () {
|
||||
return this.deepExtend (super.describe (), {
|
||||
'id': 'bibox',
|
||||
'name': 'Bibox',
|
||||
'countries': [ 'CN', 'US', 'KR' ],
|
||||
'version': 'v1',
|
||||
'has': {
|
||||
'CORS': false,
|
||||
'publicAPI': false,
|
||||
'fetchBalance': true,
|
||||
'fetchDeposits': true,
|
||||
'fetchWithdrawals': true,
|
||||
'fetchCurrencies': true,
|
||||
'fetchDepositAddress': true,
|
||||
'fetchFundingFees': true,
|
||||
'fetchTickers': true,
|
||||
'fetchOrder': true,
|
||||
'fetchOpenOrders': true,
|
||||
'fetchClosedOrders': true,
|
||||
'fetchMyTrades': true,
|
||||
'fetchOHLCV': true,
|
||||
'createMarketOrder': false, // or they will return https://github.com/ccxt/ccxt/issues/2338
|
||||
'withdraw': true,
|
||||
},
|
||||
'timeframes': {
|
||||
'1m': '1min',
|
||||
'5m': '5min',
|
||||
'15m': '15min',
|
||||
'30m': '30min',
|
||||
'1h': '1hour',
|
||||
'2h': '2hour',
|
||||
'4h': '4hour',
|
||||
'6h': '6hour',
|
||||
'12h': '12hour',
|
||||
'1d': 'day',
|
||||
'1w': 'week',
|
||||
},
|
||||
'urls': {
|
||||
'logo': 'https://user-images.githubusercontent.com/51840849/77257418-3262b000-6c85-11ea-8fb8-20bdf20b3592.jpg',
|
||||
'api': 'https://api.bibox.com',
|
||||
'www': 'https://www.bibox.com',
|
||||
'doc': [
|
||||
'https://biboxcom.github.io/en/',
|
||||
],
|
||||
'fees': 'https://bibox.zendesk.com/hc/en-us/articles/360002336133',
|
||||
'referral': 'https://w2.bibox.com/login/register?invite_code=05Kj3I',
|
||||
},
|
||||
'api': {
|
||||
'public': {
|
||||
'post': [
|
||||
// TODO: rework for full endpoint/cmd paths here
|
||||
'mdata',
|
||||
],
|
||||
'get': [
|
||||
'cquery',
|
||||
'mdata',
|
||||
],
|
||||
},
|
||||
'private': {
|
||||
'post': [
|
||||
'cquery',
|
||||
'ctrade',
|
||||
'user',
|
||||
'orderpending',
|
||||
'transfer',
|
||||
],
|
||||
},
|
||||
'v2private': {
|
||||
'post': [
|
||||
'assets/transfer/spot',
|
||||
],
|
||||
},
|
||||
},
|
||||
'fees': {
|
||||
'trading': {
|
||||
'tierBased': false,
|
||||
'percentage': true,
|
||||
'taker': 0.001,
|
||||
'maker': 0.0008,
|
||||
},
|
||||
'funding': {
|
||||
'tierBased': false,
|
||||
'percentage': false,
|
||||
'withdraw': {},
|
||||
'deposit': {},
|
||||
},
|
||||
},
|
||||
'exceptions': {
|
||||
'2011': AccountSuspended, // Account is locked
|
||||
'2015': AuthenticationError, // Google authenticator is wrong
|
||||
'2021': InsufficientFunds, // Insufficient balance available for withdrawal
|
||||
'2027': InsufficientFunds, // Insufficient balance available (for trade)
|
||||
'2033': OrderNotFound, // operation failed! Orders have been completed or revoked
|
||||
'2067': InvalidOrder, // Does not support market orders
|
||||
'2068': InvalidOrder, // The number of orders can not be less than
|
||||
'2085': InvalidOrder, // Order quantity is too small
|
||||
'3012': AuthenticationError, // invalid apiKey
|
||||
'3024': PermissionDenied, // wrong apikey permissions
|
||||
'3025': AuthenticationError, // signature failed
|
||||
'4000': ExchangeNotAvailable, // current network is unstable
|
||||
'4003': DDoSProtection, // server busy please try again later
|
||||
},
|
||||
'commonCurrencies': {
|
||||
'KEY': 'Bihu',
|
||||
'MTC': 'MTC Mesh Network', // conflict with MTC Docademic doc.com Token https://github.com/ccxt/ccxt/issues/6081 https://github.com/ccxt/ccxt/issues/3025
|
||||
'PAI': 'PCHAIN',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async fetchMarkets (params = {}) {
|
||||
const request = {
|
||||
'cmd': 'marketAll',
|
||||
};
|
||||
const response = await this.publicGetMdata (this.extend (request, params));
|
||||
//
|
||||
// {
|
||||
// "result": [
|
||||
// {
|
||||
// "is_hide":0,
|
||||
// "high_cny":"1.9478",
|
||||
// "amount":"272.41",
|
||||
// "coin_symbol":"BIX",
|
||||
// "last":"0.00002487",
|
||||
// "currency_symbol":"BTC",
|
||||
// "change":"+0.00000073",
|
||||
// "low_cny":"1.7408",
|
||||
// "base_last_cny":"1.84538041",
|
||||
// "area_id":7,
|
||||
// "percent":"+3.02%",
|
||||
// "last_cny":"1.8454",
|
||||
// "high":"0.00002625",
|
||||
// "low":"0.00002346",
|
||||
// "pair_type":0,
|
||||
// "last_usd":"0.2686",
|
||||
// "vol24H":"10940613",
|
||||
// "id":1,
|
||||
// "high_usd":"0.2835",
|
||||
// "low_usd":"0.2534"
|
||||
// }
|
||||
// ],
|
||||
// "cmd":"marketAll",
|
||||
// "ver":"1.1"
|
||||
// }
|
||||
//
|
||||
const markets = this.safeValue (response, 'result');
|
||||
const result = [];
|
||||
for (let i = 0; i < markets.length; i++) {
|
||||
const market = markets[i];
|
||||
const numericId = this.safeInteger (market, 'id');
|
||||
const baseId = this.safeString (market, 'coin_symbol');
|
||||
const quoteId = this.safeString (market, 'currency_symbol');
|
||||
const base = this.safeCurrencyCode (baseId);
|
||||
const quote = this.safeCurrencyCode (quoteId);
|
||||
const symbol = base + '/' + quote;
|
||||
const id = baseId + '_' + quoteId;
|
||||
const precision = {
|
||||
'amount': 4,
|
||||
'price': 8,
|
||||
};
|
||||
result.push ({
|
||||
'id': id,
|
||||
'numericId': numericId,
|
||||
'symbol': symbol,
|
||||
'base': base,
|
||||
'quote': quote,
|
||||
'baseId': baseId,
|
||||
'quoteId': quoteId,
|
||||
'active': true,
|
||||
'info': market,
|
||||
'precision': precision,
|
||||
'limits': {
|
||||
'amount': {
|
||||
'min': Math.pow (10, -precision['amount']),
|
||||
'max': undefined,
|
||||
},
|
||||
'price': {
|
||||
'min': Math.pow (10, -precision['price']),
|
||||
'max': undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
parseTicker (ticker, market = undefined) {
|
||||
// we don't set values that are not defined by the exchange
|
||||
const timestamp = this.safeInteger (ticker, 'timestamp');
|
||||
let symbol = undefined;
|
||||
if (market !== undefined) {
|
||||
symbol = market['symbol'];
|
||||
} else {
|
||||
const baseId = this.safeString (ticker, 'coin_symbol');
|
||||
const quoteId = this.safeString (ticker, 'currency_symbol');
|
||||
const base = this.safeCurrencyCode (baseId);
|
||||
const quote = this.safeCurrencyCode (quoteId);
|
||||
symbol = base + '/' + quote;
|
||||
}
|
||||
const last = this.safeFloat (ticker, 'last');
|
||||
const change = this.safeFloat (ticker, 'change');
|
||||
const baseVolume = this.safeFloat2 (ticker, 'vol', 'vol24H');
|
||||
let open = undefined;
|
||||
if ((last !== undefined) && (change !== undefined)) {
|
||||
open = last - change;
|
||||
}
|
||||
let percentage = this.safeString (ticker, 'percent');
|
||||
if (percentage !== undefined) {
|
||||
percentage = percentage.replace ('%', '');
|
||||
percentage = parseFloat (percentage);
|
||||
}
|
||||
return {
|
||||
'symbol': symbol,
|
||||
'timestamp': timestamp,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'high': this.safeFloat (ticker, 'high'),
|
||||
'low': this.safeFloat (ticker, 'low'),
|
||||
'bid': this.safeFloat (ticker, 'buy'),
|
||||
'bidVolume': undefined,
|
||||
'ask': this.safeFloat (ticker, 'sell'),
|
||||
'askVolume': undefined,
|
||||
'vwap': undefined,
|
||||
'open': open,
|
||||
'close': last,
|
||||
'last': last,
|
||||
'previousClose': undefined,
|
||||
'change': change,
|
||||
'percentage': percentage,
|
||||
'average': undefined,
|
||||
'baseVolume': baseVolume,
|
||||
'quoteVolume': this.safeFloat (ticker, 'amount'),
|
||||
'info': ticker,
|
||||
};
|
||||
}
|
||||
|
||||
async fetchTicker (symbol, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const request = {
|
||||
'cmd': 'ticker',
|
||||
'pair': market['id'],
|
||||
};
|
||||
const response = await this.publicGetMdata (this.extend (request, params));
|
||||
return this.parseTicker (response['result'], market);
|
||||
}
|
||||
|
||||
parseTickers (rawTickers, symbols = undefined) {
|
||||
const tickers = [];
|
||||
for (let i = 0; i < rawTickers.length; i++) {
|
||||
const ticker = this.parseTicker (rawTickers[i]);
|
||||
if ((symbols === undefined) || (this.inArray (ticker['symbol'], symbols))) {
|
||||
tickers.push (ticker);
|
||||
}
|
||||
}
|
||||
return tickers;
|
||||
}
|
||||
|
||||
async fetchTickers (symbols = undefined, params = {}) {
|
||||
const request = {
|
||||
'cmd': 'marketAll',
|
||||
};
|
||||
const response = await this.publicGetMdata (this.extend (request, params));
|
||||
const tickers = this.parseTickers (response['result'], symbols);
|
||||
return this.indexBy (tickers, 'symbol');
|
||||
}
|
||||
|
||||
parseTrade (trade, market = undefined) {
|
||||
const timestamp = this.safeInteger2 (trade, 'time', 'createdAt');
|
||||
let side = this.safeInteger2 (trade, 'side', 'order_side');
|
||||
side = (side === 1) ? 'buy' : 'sell';
|
||||
let symbol = undefined;
|
||||
if (market === undefined) {
|
||||
let marketId = this.safeString (trade, 'pair');
|
||||
if (marketId === undefined) {
|
||||
const baseId = this.safeString (trade, 'coin_symbol');
|
||||
const quoteId = this.safeString (trade, 'currency_symbol');
|
||||
if ((baseId !== undefined) && (quoteId !== undefined)) {
|
||||
marketId = baseId + '_' + quoteId;
|
||||
}
|
||||
}
|
||||
if (marketId in this.markets_by_id) {
|
||||
market = this.markets_by_id[marketId];
|
||||
}
|
||||
}
|
||||
if (market !== undefined) {
|
||||
symbol = market['symbol'];
|
||||
}
|
||||
let fee = undefined;
|
||||
const feeCost = this.safeFloat (trade, 'fee');
|
||||
let feeCurrency = this.safeString (trade, 'fee_symbol');
|
||||
if (feeCurrency !== undefined) {
|
||||
if (feeCurrency in this.currencies_by_id) {
|
||||
feeCurrency = this.currencies_by_id[feeCurrency]['code'];
|
||||
} else {
|
||||
feeCurrency = this.safeCurrencyCode (feeCurrency);
|
||||
}
|
||||
}
|
||||
const feeRate = undefined; // todo: deduce from market if market is defined
|
||||
const price = this.safeFloat (trade, 'price');
|
||||
const amount = this.safeFloat (trade, 'amount');
|
||||
let cost = undefined;
|
||||
if (price !== undefined && amount !== undefined) {
|
||||
cost = price * amount;
|
||||
}
|
||||
if (feeCost !== undefined) {
|
||||
fee = {
|
||||
'cost': feeCost,
|
||||
'currency': feeCurrency,
|
||||
'rate': feeRate,
|
||||
};
|
||||
}
|
||||
const id = this.safeString (trade, 'id');
|
||||
return {
|
||||
'info': trade,
|
||||
'id': id,
|
||||
'order': undefined, // Bibox does not have it (documented) yet
|
||||
'timestamp': timestamp,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'symbol': symbol,
|
||||
'type': 'limit',
|
||||
'takerOrMaker': undefined,
|
||||
'side': side,
|
||||
'price': price,
|
||||
'amount': amount,
|
||||
'cost': cost,
|
||||
'fee': fee,
|
||||
};
|
||||
}
|
||||
|
||||
async fetchTrades (symbol, since = undefined, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const request = {
|
||||
'cmd': 'deals',
|
||||
'pair': market['id'],
|
||||
};
|
||||
if (limit !== undefined) {
|
||||
request['size'] = limit; // default = 200
|
||||
}
|
||||
const response = await this.publicGetMdata (this.extend (request, params));
|
||||
return this.parseTrades (response['result'], market, since, limit);
|
||||
}
|
||||
|
||||
async fetchOrderBook (symbol, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const request = {
|
||||
'cmd': 'depth',
|
||||
'pair': market['id'],
|
||||
};
|
||||
if (limit !== undefined) {
|
||||
request['size'] = limit; // default = 200
|
||||
}
|
||||
const response = await this.publicGetMdata (this.extend (request, params));
|
||||
return this.parseOrderBook (response['result'], this.safeFloat (response['result'], 'update_time'), 'bids', 'asks', 'price', 'volume');
|
||||
}
|
||||
|
||||
parseOHLCV (ohlcv, market = undefined, timeframe = '1m', since = undefined, limit = undefined) {
|
||||
return [
|
||||
ohlcv['time'],
|
||||
this.safeFloat (ohlcv, 'open'),
|
||||
this.safeFloat (ohlcv, 'high'),
|
||||
this.safeFloat (ohlcv, 'low'),
|
||||
this.safeFloat (ohlcv, 'close'),
|
||||
this.safeFloat (ohlcv, 'vol'),
|
||||
];
|
||||
}
|
||||
|
||||
async fetchOHLCV (symbol, timeframe = '1m', since = undefined, limit = 1000, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const request = {
|
||||
'cmd': 'kline',
|
||||
'pair': market['id'],
|
||||
'period': this.timeframes[timeframe],
|
||||
'size': limit,
|
||||
};
|
||||
const response = await this.publicGetMdata (this.extend (request, params));
|
||||
return this.parseOHLCVs (response['result'], market, timeframe, since, limit);
|
||||
}
|
||||
|
||||
async fetchCurrencies (params = {}) {
|
||||
if (!this.apiKey || !this.secret) {
|
||||
throw new AuthenticationError (this.id + " fetchCurrencies is an authenticated endpoint, therefore it requires 'apiKey' and 'secret' credentials. If you don't need currency details, set exchange.has['fetchCurrencies'] = false before calling its methods.");
|
||||
}
|
||||
const request = {
|
||||
'cmd': 'transfer/coinList',
|
||||
'body': {},
|
||||
};
|
||||
const response = await this.privatePostTransfer (this.extend (request, params));
|
||||
const currencies = this.safeValue (response, 'result');
|
||||
const result = {};
|
||||
for (let i = 0; i < currencies.length; i++) {
|
||||
const currency = currencies[i];
|
||||
const id = this.safeString (currency, 'symbol');
|
||||
const name = currency['name']; // contains hieroglyphs causing python ASCII bug
|
||||
const code = this.safeCurrencyCode (id);
|
||||
const precision = 8;
|
||||
const deposit = this.safeValue (currency, 'enable_deposit');
|
||||
const withdraw = this.safeValue (currency, 'enable_withdraw');
|
||||
const active = (deposit && withdraw);
|
||||
result[code] = {
|
||||
'id': id,
|
||||
'code': code,
|
||||
'info': currency,
|
||||
'name': name,
|
||||
'active': active,
|
||||
'fee': undefined,
|
||||
'precision': precision,
|
||||
'limits': {
|
||||
'amount': {
|
||||
'min': Math.pow (10, -precision),
|
||||
'max': Math.pow (10, precision),
|
||||
},
|
||||
'price': {
|
||||
'min': Math.pow (10, -precision),
|
||||
'max': Math.pow (10, precision),
|
||||
},
|
||||
'cost': {
|
||||
'min': undefined,
|
||||
'max': undefined,
|
||||
},
|
||||
'withdraw': {
|
||||
'min': undefined,
|
||||
'max': Math.pow (10, precision),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async fetchBalance (params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const type = this.safeString (params, 'type', 'assets');
|
||||
params = this.omit (params, 'type');
|
||||
const request = {
|
||||
'cmd': 'transfer/' + type, // assets, mainAssets
|
||||
'body': this.extend ({
|
||||
'select': 1, // return full info
|
||||
}, params),
|
||||
};
|
||||
const response = await this.privatePostTransfer (request);
|
||||
const balances = this.safeValue (response, 'result');
|
||||
const result = { 'info': balances };
|
||||
let indexed = undefined;
|
||||
if ('assets_list' in balances) {
|
||||
indexed = this.indexBy (balances['assets_list'], 'coin_symbol');
|
||||
} else {
|
||||
indexed = balances;
|
||||
}
|
||||
const keys = Object.keys (indexed);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const id = keys[i];
|
||||
let code = id.toUpperCase ();
|
||||
if (code.indexOf ('TOTAL_') >= 0) {
|
||||
code = code.slice (6);
|
||||
}
|
||||
if (code in this.currencies_by_id) {
|
||||
code = this.currencies_by_id[code]['code'];
|
||||
}
|
||||
const account = this.account ();
|
||||
let balance = indexed[id];
|
||||
if (typeof balance === 'string') {
|
||||
balance = parseFloat (balance);
|
||||
account['free'] = balance;
|
||||
account['used'] = 0.0;
|
||||
account['total'] = balance;
|
||||
} else {
|
||||
account['free'] = this.safeFloat (balance, 'balance');
|
||||
account['used'] = this.safeFloat (balance, 'freeze');
|
||||
}
|
||||
result[code] = account;
|
||||
}
|
||||
return this.parseBalance (result);
|
||||
}
|
||||
|
||||
async fetchDeposits (code = undefined, since = undefined, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
let currency = undefined;
|
||||
const request = {
|
||||
'page': 1,
|
||||
};
|
||||
if (code !== undefined) {
|
||||
currency = this.currency (code);
|
||||
request['symbol'] = currency['id'];
|
||||
}
|
||||
if (limit !== undefined) {
|
||||
request['size'] = limit;
|
||||
} else {
|
||||
request['size'] = 100;
|
||||
}
|
||||
const response = await this.privatePostTransfer ({
|
||||
'cmd': 'transfer/transferInList',
|
||||
'body': this.extend (request, params),
|
||||
});
|
||||
const deposits = this.safeValue (response['result'], 'items', []);
|
||||
for (let i = 0; i < deposits.length; i++) {
|
||||
deposits[i]['type'] = 'deposit';
|
||||
}
|
||||
return this.parseTransactions (deposits, currency, since, limit);
|
||||
}
|
||||
|
||||
async fetchWithdrawals (code = undefined, since = undefined, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
let currency = undefined;
|
||||
const request = {
|
||||
'page': 1,
|
||||
};
|
||||
if (code !== undefined) {
|
||||
currency = this.currency (code);
|
||||
request['symbol'] = currency['id'];
|
||||
}
|
||||
if (limit !== undefined) {
|
||||
request['size'] = limit;
|
||||
} else {
|
||||
request['size'] = 100;
|
||||
}
|
||||
const response = await this.privatePostTransfer ({
|
||||
'cmd': 'transfer/transferOutList',
|
||||
'body': this.extend (request, params),
|
||||
});
|
||||
const withdrawals = this.safeValue (response['result'], 'items', []);
|
||||
for (let i = 0; i < withdrawals.length; i++) {
|
||||
withdrawals[i]['type'] = 'withdrawal';
|
||||
}
|
||||
return this.parseTransactions (withdrawals, currency, since, limit);
|
||||
}
|
||||
|
||||
parseTransaction (transaction, currency = undefined) {
|
||||
//
|
||||
// fetchDeposits
|
||||
//
|
||||
// {
|
||||
// 'id': 1023291,
|
||||
// 'coin_symbol': 'ETH',
|
||||
// 'to_address': '0x7263....',
|
||||
// 'amount': '0.49170000',
|
||||
// 'confirmCount': '16',
|
||||
// 'createdAt': 1553123867000,
|
||||
// 'status': 2
|
||||
// }
|
||||
//
|
||||
// fetchWithdrawals
|
||||
//
|
||||
// {
|
||||
// 'id': 521844,
|
||||
// 'coin_symbol': 'ETH',
|
||||
// 'to_address': '0xfd4e....',
|
||||
// 'addr_remark': '',
|
||||
// 'amount': '0.39452750',
|
||||
// 'fee': '0.00600000',
|
||||
// 'createdAt': 1553226906000,
|
||||
// 'memo': '',
|
||||
// 'status': 3
|
||||
// }
|
||||
//
|
||||
const id = this.safeString (transaction, 'id');
|
||||
const address = this.safeString (transaction, 'to_address');
|
||||
const currencyId = this.safeString (transaction, 'coin_symbol');
|
||||
const code = this.safeCurrencyCode (currencyId, currency);
|
||||
const timestamp = this.safeString (transaction, 'createdAt');
|
||||
let tag = this.safeString (transaction, 'addr_remark');
|
||||
const type = this.safeString (transaction, 'type');
|
||||
const status = this.parseTransactionStatusByType (this.safeString (transaction, 'status'), type);
|
||||
const amount = this.safeFloat (transaction, 'amount');
|
||||
let feeCost = this.safeFloat (transaction, 'fee');
|
||||
if (type === 'deposit') {
|
||||
feeCost = 0;
|
||||
tag = undefined;
|
||||
}
|
||||
const fee = {
|
||||
'cost': feeCost,
|
||||
'currency': code,
|
||||
};
|
||||
return {
|
||||
'info': transaction,
|
||||
'id': id,
|
||||
'txid': undefined,
|
||||
'timestamp': timestamp,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'address': address,
|
||||
'tag': tag,
|
||||
'type': type,
|
||||
'amount': amount,
|
||||
'currency': code,
|
||||
'status': status,
|
||||
'updated': undefined,
|
||||
'fee': fee,
|
||||
};
|
||||
}
|
||||
|
||||
parseTransactionStatusByType (status, type = undefined) {
|
||||
const statuses = {
|
||||
'deposit': {
|
||||
'1': 'pending',
|
||||
'2': 'ok',
|
||||
},
|
||||
'withdrawal': {
|
||||
'0': 'pending',
|
||||
'3': 'ok',
|
||||
},
|
||||
};
|
||||
return this.safeString (this.safeValue (statuses, type, {}), status, status);
|
||||
}
|
||||
|
||||
async createOrder (symbol, type, side, amount, price = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const orderType = (type === 'limit') ? 2 : 1;
|
||||
const orderSide = (side === 'buy') ? 1 : 2;
|
||||
const request = {
|
||||
'cmd': 'orderpending/trade',
|
||||
'body': this.extend ({
|
||||
'pair': market['id'],
|
||||
'account_type': 0,
|
||||
'order_type': orderType,
|
||||
'order_side': orderSide,
|
||||
'pay_bix': 0,
|
||||
'amount': amount,
|
||||
'price': price,
|
||||
}, params),
|
||||
};
|
||||
const response = await this.privatePostOrderpending (request);
|
||||
return {
|
||||
'info': response,
|
||||
'id': this.safeString (response, 'result'),
|
||||
};
|
||||
}
|
||||
|
||||
async cancelOrder (id, symbol = undefined, params = {}) {
|
||||
const request = {
|
||||
'cmd': 'orderpending/cancelTrade',
|
||||
'body': this.extend ({
|
||||
'orders_id': id,
|
||||
}, params),
|
||||
};
|
||||
const response = await this.privatePostOrderpending (request);
|
||||
return response;
|
||||
}
|
||||
|
||||
async fetchOrder (id, symbol = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const request = {
|
||||
'cmd': 'orderpending/order',
|
||||
'body': this.extend ({
|
||||
'id': id.toString (),
|
||||
'account_type': 0, // 0 = spot account
|
||||
}, params),
|
||||
};
|
||||
const response = await this.privatePostOrderpending (request);
|
||||
const order = this.safeValue (response, 'result');
|
||||
if (this.isEmpty (order)) {
|
||||
throw new OrderNotFound (this.id + ' order ' + id + ' not found');
|
||||
}
|
||||
return this.parseOrder (order);
|
||||
}
|
||||
|
||||
parseOrder (order, market = undefined) {
|
||||
let symbol = undefined;
|
||||
if (market === undefined) {
|
||||
let marketId = undefined;
|
||||
const baseId = this.safeString (order, 'coin_symbol');
|
||||
const quoteId = this.safeString (order, 'currency_symbol');
|
||||
if ((baseId !== undefined) && (quoteId !== undefined)) {
|
||||
marketId = baseId + '_' + quoteId;
|
||||
}
|
||||
if (marketId in this.markets_by_id) {
|
||||
market = this.markets_by_id[marketId];
|
||||
}
|
||||
}
|
||||
if (market !== undefined) {
|
||||
symbol = market['symbol'];
|
||||
}
|
||||
const type = (order['order_type'] === 1) ? 'market' : 'limit';
|
||||
const timestamp = order['createdAt'];
|
||||
const price = this.safeFloat (order, 'price');
|
||||
const average = this.safeFloat (order, 'deal_price');
|
||||
const filled = this.safeFloat (order, 'deal_amount');
|
||||
const amount = this.safeFloat (order, 'amount');
|
||||
let cost = this.safeFloat2 (order, 'deal_money', 'money');
|
||||
let remaining = undefined;
|
||||
if (filled !== undefined) {
|
||||
if (amount !== undefined) {
|
||||
remaining = amount - filled;
|
||||
}
|
||||
if (cost === undefined) {
|
||||
cost = price * filled;
|
||||
}
|
||||
}
|
||||
const side = (order['order_side'] === 1) ? 'buy' : 'sell';
|
||||
const status = this.parseOrderStatus (this.safeString (order, 'status'));
|
||||
const id = this.safeString (order, 'id');
|
||||
const feeCost = this.safeFloat (order, 'fee');
|
||||
let fee = undefined;
|
||||
if (feeCost !== undefined) {
|
||||
fee = {
|
||||
'cost': feeCost,
|
||||
'currency': undefined,
|
||||
};
|
||||
}
|
||||
cost = cost ? cost : (parseFloat (price) * filled);
|
||||
return {
|
||||
'info': order,
|
||||
'id': id,
|
||||
'clientOrderId': undefined,
|
||||
'timestamp': timestamp,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'lastTradeTimestamp': undefined,
|
||||
'symbol': symbol,
|
||||
'type': type,
|
||||
'side': side,
|
||||
'price': price,
|
||||
'amount': amount,
|
||||
'cost': cost,
|
||||
'average': average,
|
||||
'filled': filled,
|
||||
'remaining': remaining,
|
||||
'status': status,
|
||||
'fee': fee,
|
||||
'trades': undefined,
|
||||
};
|
||||
}
|
||||
|
||||
parseOrderStatus (status) {
|
||||
const statuses = {
|
||||
// original comments from bibox:
|
||||
'1': 'open', // pending
|
||||
'2': 'open', // part completed
|
||||
'3': 'closed', // completed
|
||||
'4': 'canceled', // part canceled
|
||||
'5': 'canceled', // canceled
|
||||
'6': 'canceled', // canceling
|
||||
};
|
||||
return this.safeString (statuses, status, status);
|
||||
}
|
||||
|
||||
async fetchOpenOrders (symbol = undefined, since = undefined, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
let market = undefined;
|
||||
let pair = undefined;
|
||||
if (symbol !== undefined) {
|
||||
market = this.market (symbol);
|
||||
pair = market['id'];
|
||||
}
|
||||
const size = limit ? limit : 200;
|
||||
const request = {
|
||||
'cmd': 'orderpending/orderPendingList',
|
||||
'body': this.extend ({
|
||||
'pair': pair,
|
||||
'account_type': 0, // 0 - regular, 1 - margin
|
||||
'page': 1,
|
||||
'size': size,
|
||||
}, params),
|
||||
};
|
||||
const response = await this.privatePostOrderpending (request);
|
||||
const orders = this.safeValue (response['result'], 'items', []);
|
||||
return this.parseOrders (orders, market, since, limit);
|
||||
}
|
||||
|
||||
async fetchClosedOrders (symbol = undefined, since = undefined, limit = 200, params = {}) {
|
||||
if (symbol === undefined) {
|
||||
throw new ArgumentsRequired (this.id + ' fetchClosedOrders requires a `symbol` argument');
|
||||
}
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const request = {
|
||||
'cmd': 'orderpending/pendingHistoryList',
|
||||
'body': this.extend ({
|
||||
'pair': market['id'],
|
||||
'account_type': 0, // 0 - regular, 1 - margin
|
||||
'page': 1,
|
||||
'size': limit,
|
||||
}, params),
|
||||
};
|
||||
const response = await this.privatePostOrderpending (request);
|
||||
const orders = this.safeValue (response['result'], 'items', []);
|
||||
return this.parseOrders (orders, market, since, limit);
|
||||
}
|
||||
|
||||
async fetchMyTrades (symbol = undefined, since = undefined, limit = undefined, params = {}) {
|
||||
if (symbol === undefined) {
|
||||
throw new ArgumentsRequired (this.id + ' fetchMyTrades requires a `symbol` argument');
|
||||
}
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const size = limit ? limit : 200;
|
||||
const request = {
|
||||
'cmd': 'orderpending/orderHistoryList',
|
||||
'body': this.extend ({
|
||||
'pair': market['id'],
|
||||
'account_type': 0, // 0 - regular, 1 - margin
|
||||
'page': 1,
|
||||
'size': size,
|
||||
'coin_symbol': market['baseId'],
|
||||
'currency_symbol': market['quoteId'],
|
||||
}, params),
|
||||
};
|
||||
const response = await this.privatePostOrderpending (request);
|
||||
const trades = this.safeValue (response['result'], 'items', []);
|
||||
return this.parseTrades (trades, market, since, limit);
|
||||
}
|
||||
|
||||
async fetchDepositAddress (code, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const currency = this.currency (code);
|
||||
const request = {
|
||||
'cmd': 'transfer/transferIn',
|
||||
'body': this.extend ({
|
||||
'coin_symbol': currency['id'],
|
||||
}, params),
|
||||
};
|
||||
const response = await this.privatePostTransfer (request);
|
||||
//
|
||||
// {
|
||||
// "result":"3Jx6RZ9TNMsAoy9NUzBwZf68QBppDruSKW","cmd":"transfer/transferIn"
|
||||
// }
|
||||
//
|
||||
// {
|
||||
// "result":"{\"account\":\"PERSONALLY OMITTED\",\"memo\":\"PERSONALLY OMITTED\"}","cmd":"transfer/transferIn"
|
||||
// }
|
||||
//
|
||||
const result = this.safeString (response, 'result');
|
||||
let address = result;
|
||||
let tag = undefined;
|
||||
if (this.isJsonEncodedObject (result)) {
|
||||
const parsed = JSON.parse (result);
|
||||
address = this.safeString (parsed, 'account');
|
||||
tag = this.safeString (parsed, 'memo');
|
||||
}
|
||||
return {
|
||||
'currency': code,
|
||||
'address': address,
|
||||
'tag': tag,
|
||||
'info': response,
|
||||
};
|
||||
}
|
||||
|
||||
async withdraw (code, amount, address, tag = undefined, params = {}) {
|
||||
this.checkAddress (address);
|
||||
await this.loadMarkets ();
|
||||
const currency = this.currency (code);
|
||||
if (this.password === undefined) {
|
||||
if (!('trade_pwd' in params)) {
|
||||
throw new ExchangeError (this.id + ' withdraw() requires this.password set on the exchange instance or a trade_pwd parameter');
|
||||
}
|
||||
}
|
||||
if (!('totp_code' in params)) {
|
||||
throw new ExchangeError (this.id + ' withdraw() requires a totp_code parameter for 2FA authentication');
|
||||
}
|
||||
const request = {
|
||||
'trade_pwd': this.password,
|
||||
'coin_symbol': currency['id'],
|
||||
'amount': amount,
|
||||
'addr': address,
|
||||
};
|
||||
if (tag !== undefined) {
|
||||
request['address_remark'] = tag;
|
||||
}
|
||||
const response = await this.privatePostTransfer ({
|
||||
'cmd': 'transfer/transferOut',
|
||||
'body': this.extend (request, params),
|
||||
});
|
||||
return {
|
||||
'info': response,
|
||||
'id': undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async fetchFundingFees (codes = undefined, params = {}) {
|
||||
// by default it will try load withdrawal fees of all currencies (with separate requests)
|
||||
// however if you define codes = [ 'ETH', 'BTC' ] in args it will only load those
|
||||
await this.loadMarkets ();
|
||||
const withdrawFees = {};
|
||||
const info = {};
|
||||
if (codes === undefined) {
|
||||
codes = Object.keys (this.currencies);
|
||||
}
|
||||
for (let i = 0; i < codes.length; i++) {
|
||||
const code = codes[i];
|
||||
const currency = this.currency (code);
|
||||
const request = {
|
||||
'cmd': 'transfer/coinConfig',
|
||||
'body': this.extend ({
|
||||
'coin_symbol': currency['id'],
|
||||
}, params),
|
||||
};
|
||||
const response = await this.privatePostTransfer (request);
|
||||
info[code] = response;
|
||||
withdrawFees[code] = this.safeFloat (response['result'], 'withdraw_fee');
|
||||
}
|
||||
return {
|
||||
'info': info,
|
||||
'withdraw': withdrawFees,
|
||||
'deposit': {},
|
||||
};
|
||||
}
|
||||
|
||||
sign (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) {
|
||||
let url = this.urls['api'] + '/' + this.version + '/' + path;
|
||||
const cmds = this.json ([ params ]);
|
||||
if (api === 'public') {
|
||||
if (method !== 'GET') {
|
||||
body = { 'cmds': cmds };
|
||||
} else if (Object.keys (params).length) {
|
||||
url += '?' + this.urlencode (params);
|
||||
}
|
||||
} else if (api === 'v2private') {
|
||||
this.checkRequiredCredentials ();
|
||||
url = this.urls['api'] + '/v2/' + path;
|
||||
const json_params = this.json (params);
|
||||
body = {
|
||||
'body': json_params,
|
||||
'apikey': this.apiKey,
|
||||
'sign': this.hmac (this.encode (json_params), this.encode (this.secret), 'md5'),
|
||||
};
|
||||
} else {
|
||||
this.checkRequiredCredentials ();
|
||||
body = {
|
||||
'cmds': cmds,
|
||||
'apikey': this.apiKey,
|
||||
'sign': this.hmac (this.encode (cmds), this.encode (this.secret), 'md5'),
|
||||
};
|
||||
}
|
||||
if (body !== undefined) {
|
||||
body = this.json (body, { 'convertArraysToObjects': true });
|
||||
}
|
||||
headers = { 'Content-Type': 'application/json' };
|
||||
return { 'url': url, 'method': method, 'body': body, 'headers': headers };
|
||||
}
|
||||
|
||||
handleErrors (code, reason, url, method, headers, body, response, requestHeaders, requestBody) {
|
||||
if (response === undefined) {
|
||||
return;
|
||||
}
|
||||
if ('error' in response) {
|
||||
if ('code' in response['error']) {
|
||||
const code = this.safeString (response['error'], 'code');
|
||||
const feedback = this.id + ' ' + body;
|
||||
this.throwExactlyMatchedException (this.exceptions, code, feedback);
|
||||
throw new ExchangeError (feedback);
|
||||
}
|
||||
throw new ExchangeError (this.id + ' ' + body);
|
||||
}
|
||||
if (!('result' in response)) {
|
||||
throw new ExchangeError (this.id + ' ' + body);
|
||||
}
|
||||
}
|
||||
|
||||
async request (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) {
|
||||
const response = await this.fetch2 (path, api, method, params, headers, body);
|
||||
if (method === 'GET') {
|
||||
return response;
|
||||
} else {
|
||||
return response['result'][0];
|
||||
}
|
||||
}
|
||||
};
|
||||
+1256
File diff suppressed because it is too large.
Load diff
+2033
File diff suppressed because it is too large.
Load diff
+53
@@ -0,0 +1,53 @@
|
||||
'use strict';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const binance = require ('./binance.js');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
module.exports = class binanceje extends binance {
|
||||
describe () {
|
||||
return this.deepExtend (super.describe (), {
|
||||
'id': 'binanceje',
|
||||
'name': 'Binance Jersey',
|
||||
'countries': [ 'JE' ], // Jersey
|
||||
'certified': false,
|
||||
'pro': true,
|
||||
'urls': {
|
||||
'logo': 'https://user-images.githubusercontent.com/1294454/54874009-d526eb00-4df3-11e9-928c-ce6a2b914cd1.jpg',
|
||||
'api': {
|
||||
'web': 'https://www.binance.je',
|
||||
'wapi': 'https://api.binance.je/wapi/v3',
|
||||
'public': 'https://api.binance.je/api/v1',
|
||||
'private': 'https://api.binance.je/api/v3',
|
||||
'v3': 'https://api.binance.je/api/v3',
|
||||
'v1': 'https://api.binance.je/api/v1',
|
||||
},
|
||||
'www': 'https://www.binance.je',
|
||||
'referral': 'https://www.binance.je/?ref=35047921',
|
||||
'doc': 'https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md',
|
||||
'fees': 'https://www.binance.je/fees.html',
|
||||
},
|
||||
'fees': {
|
||||
'trading': {
|
||||
'tierBased': false,
|
||||
'percentage': true,
|
||||
'taker': 0.0005,
|
||||
'maker': 0.0005,
|
||||
},
|
||||
// should be deleted, these are outdated and inaccurate
|
||||
'funding': {
|
||||
'tierBased': false,
|
||||
'percentage': false,
|
||||
'withdraw': {
|
||||
'BTC': 0.0005,
|
||||
'ETH': 0.01,
|
||||
},
|
||||
'deposit': {},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
'use strict';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const binance = require ('./binance.js');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
module.exports = class binanceus extends binance {
|
||||
describe () {
|
||||
return this.deepExtend (super.describe (), {
|
||||
'id': 'binanceus',
|
||||
'name': 'Binance US',
|
||||
'countries': [ 'US' ], // US
|
||||
'certified': false,
|
||||
'pro': true,
|
||||
'urls': {
|
||||
'logo': 'https://user-images.githubusercontent.com/1294454/65177307-217b7c80-da5f-11e9-876e-0b748ba0a358.jpg',
|
||||
'api': {
|
||||
'web': 'https://www.binance.us',
|
||||
'wapi': 'https://api.binance.us/wapi/v3',
|
||||
'public': 'https://api.binance.us/api/v1',
|
||||
'private': 'https://api.binance.us/api/v3',
|
||||
'v3': 'https://api.binance.us/api/v3',
|
||||
'v1': 'https://api.binance.us/api/v1',
|
||||
},
|
||||
'www': 'https://www.binance.us',
|
||||
'referral': 'https://www.binance.us/?ref=35005074',
|
||||
'doc': 'https://github.com/binance-us/binance-official-api-docs',
|
||||
'fees': 'https://www.binance.us/en/fee/schedule',
|
||||
},
|
||||
'fees': {
|
||||
'trading': {
|
||||
'tierBased': true,
|
||||
'percentage': true,
|
||||
'taker': 0.001, // 0.1% trading fee, zero fees for all trading pairs before November 1
|
||||
'maker': 0.001, // 0.1% trading fee, zero fees for all trading pairs before November 1
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
+420
@@ -0,0 +1,420 @@
|
||||
'use strict';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const Exchange = require ('./base/Exchange');
|
||||
const { ArgumentsRequired, ExchangeError } = require ('./base/errors');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
module.exports = class bit2c extends Exchange {
|
||||
describe () {
|
||||
return this.deepExtend (super.describe (), {
|
||||
'id': 'bit2c',
|
||||
'name': 'Bit2C',
|
||||
'countries': [ 'IL' ], // Israel
|
||||
'rateLimit': 3000,
|
||||
'has': {
|
||||
'CORS': false,
|
||||
'fetchOpenOrders': true,
|
||||
'fetchMyTrades': true,
|
||||
},
|
||||
'urls': {
|
||||
'logo': 'https://user-images.githubusercontent.com/1294454/27766119-3593220e-5ece-11e7-8b3a-5a041f6bcc3f.jpg',
|
||||
'api': 'https://bit2c.co.il',
|
||||
'www': 'https://www.bit2c.co.il',
|
||||
'referral': 'https://bit2c.co.il/Aff/63bfed10-e359-420c-ab5a-ad368dab0baf',
|
||||
'doc': [
|
||||
'https://www.bit2c.co.il/home/api',
|
||||
'https://github.com/OferE/bit2c',
|
||||
],
|
||||
},
|
||||
'api': {
|
||||
'public': {
|
||||
'get': [
|
||||
'Exchanges/{pair}/Ticker',
|
||||
'Exchanges/{pair}/orderbook',
|
||||
'Exchanges/{pair}/trades',
|
||||
'Exchanges/{pair}/lasttrades',
|
||||
],
|
||||
},
|
||||
'private': {
|
||||
'post': [
|
||||
'Merchant/CreateCheckout',
|
||||
'Order/AddCoinFundsRequest',
|
||||
'Order/AddFund',
|
||||
'Order/AddOrder',
|
||||
'Order/AddOrderMarketPriceBuy',
|
||||
'Order/AddOrderMarketPriceSell',
|
||||
'Order/CancelOrder',
|
||||
'Order/AddCoinFundsRequest',
|
||||
'Order/AddStopOrder',
|
||||
'Payment/GetMyId',
|
||||
'Payment/Send',
|
||||
'Payment/Pay',
|
||||
],
|
||||
'get': [
|
||||
'Account/Balance',
|
||||
'Account/Balance/v2',
|
||||
'Order/MyOrders',
|
||||
'Order/GetById',
|
||||
'Order/AccountHistory',
|
||||
'Order/OrderHistory',
|
||||
],
|
||||
},
|
||||
},
|
||||
'markets': {
|
||||
'BTC/NIS': { 'id': 'BtcNis', 'symbol': 'BTC/NIS', 'base': 'BTC', 'quote': 'NIS', 'baseId': 'Btc', 'quoteId': 'Nis' },
|
||||
'ETH/NIS': { 'id': 'EthNis', 'symbol': 'ETH/NIS', 'base': 'ETH', 'quote': 'NIS', 'baseId': 'Eth', 'quoteId': 'Nis' },
|
||||
'BCH/NIS': { 'id': 'BchabcNis', 'symbol': 'BCH/NIS', 'base': 'BCH', 'quote': 'NIS', 'baseId': 'Bchabc', 'quoteId': 'Nis' },
|
||||
'LTC/NIS': { 'id': 'LtcNis', 'symbol': 'LTC/NIS', 'base': 'LTC', 'quote': 'NIS', 'baseId': 'Ltc', 'quoteId': 'Nis' },
|
||||
'ETC/NIS': { 'id': 'EtcNis', 'symbol': 'ETC/NIS', 'base': 'ETC', 'quote': 'NIS', 'baseId': 'Etc', 'quoteId': 'Nis' },
|
||||
'BTG/NIS': { 'id': 'BtgNis', 'symbol': 'BTG/NIS', 'base': 'BTG', 'quote': 'NIS', 'baseId': 'Btg', 'quoteId': 'Nis' },
|
||||
'BSV/NIS': { 'id': 'BchsvNis', 'symbol': 'BSV/NIS', 'base': 'BSV', 'quote': 'NIS', 'baseId': 'Bchsv', 'quoteId': 'Nis' },
|
||||
'GRIN/NIS': { 'id': 'GrinNis', 'symbol': 'GRIN/NIS', 'base': 'GRIN', 'quote': 'NIS', 'baseId': 'Grin', 'quoteId': 'Nis' },
|
||||
},
|
||||
'fees': {
|
||||
'trading': {
|
||||
'maker': 0.5 / 100,
|
||||
'taker': 0.5 / 100,
|
||||
},
|
||||
},
|
||||
'options': {
|
||||
'fetchTradesMethod': 'public_get_exchanges_pair_lasttrades',
|
||||
},
|
||||
'exceptions': {
|
||||
// { "error" : "Please provide valid APIkey" }
|
||||
// { "error" : "Please provide valid nonce in Request UInt64.TryParse failed for nonce :" }
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async fetchBalance (params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const balance = await this.privateGetAccountBalanceV2 (params);
|
||||
//
|
||||
// {
|
||||
// "AVAILABLE_NIS": 0.0,
|
||||
// "NIS": 0.0,
|
||||
// "LOCKED_NIS": 0.0,
|
||||
// "AVAILABLE_BTC": 0.0,
|
||||
// "BTC": 0.0,
|
||||
// "LOCKED_BTC": 0.0,
|
||||
// "AVAILABLE_ETH": 0.0,
|
||||
// "ETH": 0.0,
|
||||
// "LOCKED_ETH": 0.0,
|
||||
// "AVAILABLE_BCHSV": 0.0,
|
||||
// "BCHSV": 0.0,
|
||||
// "LOCKED_BCHSV": 0.0,
|
||||
// "AVAILABLE_BCHABC": 0.0,
|
||||
// "BCHABC": 0.0,
|
||||
// "LOCKED_BCHABC": 0.0,
|
||||
// "AVAILABLE_LTC": 0.0,
|
||||
// "LTC": 0.0,
|
||||
// "LOCKED_LTC": 0.0,
|
||||
// "AVAILABLE_ETC": 0.0,
|
||||
// "ETC": 0.0,
|
||||
// "LOCKED_ETC": 0.0,
|
||||
// "AVAILABLE_BTG": 0.0,
|
||||
// "BTG": 0.0,
|
||||
// "LOCKED_BTG": 0.0,
|
||||
// "AVAILABLE_GRIN": 0.0,
|
||||
// "GRIN": 0.0,
|
||||
// "LOCKED_GRIN": 0.0,
|
||||
// "Fees": {
|
||||
// "BtcNis": { "FeeMaker": 1.0, "FeeTaker": 1.0 },
|
||||
// "EthNis": { "FeeMaker": 1.0, "FeeTaker": 1.0 },
|
||||
// "BchabcNis": { "FeeMaker": 1.0, "FeeTaker": 1.0 },
|
||||
// "LtcNis": { "FeeMaker": 1.0, "FeeTaker": 1.0 },
|
||||
// "EtcNis": { "FeeMaker": 1.0, "FeeTaker": 1.0 },
|
||||
// "BtgNis": { "FeeMaker": 1.0, "FeeTaker": 1.0 },
|
||||
// "LtcBtc": { "FeeMaker": 1.0, "FeeTaker": 1.0 },
|
||||
// "BchsvNis": { "FeeMaker": 1.0, "FeeTaker": 1.0 },
|
||||
// "GrinNis": { "FeeMaker": 1.0, "FeeTaker": 1.0 }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
const result = { 'info': balance };
|
||||
const codes = Object.keys (this.currencies);
|
||||
for (let i = 0; i < codes.length; i++) {
|
||||
const code = codes[i];
|
||||
const account = this.account ();
|
||||
const currencyId = this.currencyId (code);
|
||||
const uppercase = currencyId.toUpperCase ();
|
||||
if (uppercase in balance) {
|
||||
account['free'] = this.safeFloat (balance, 'AVAILABLE_' + uppercase);
|
||||
account['total'] = this.safeFloat (balance, uppercase);
|
||||
}
|
||||
result[code] = account;
|
||||
}
|
||||
return this.parseBalance (result);
|
||||
}
|
||||
|
||||
async fetchOrderBook (symbol, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const request = {
|
||||
'pair': this.marketId (symbol),
|
||||
};
|
||||
const orderbook = await this.publicGetExchangesPairOrderbook (this.extend (request, params));
|
||||
return this.parseOrderBook (orderbook);
|
||||
}
|
||||
|
||||
async fetchTicker (symbol, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const request = {
|
||||
'pair': this.marketId (symbol),
|
||||
};
|
||||
const ticker = await this.publicGetExchangesPairTicker (this.extend (request, params));
|
||||
const timestamp = this.milliseconds ();
|
||||
const averagePrice = this.safeFloat (ticker, 'av');
|
||||
const baseVolume = this.safeFloat (ticker, 'a');
|
||||
let quoteVolume = undefined;
|
||||
if (baseVolume !== undefined && averagePrice !== undefined) {
|
||||
quoteVolume = baseVolume * averagePrice;
|
||||
}
|
||||
const last = this.safeFloat (ticker, 'll');
|
||||
return {
|
||||
'symbol': symbol,
|
||||
'timestamp': timestamp,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'high': undefined,
|
||||
'low': undefined,
|
||||
'bid': this.safeFloat (ticker, 'h'),
|
||||
'bidVolume': undefined,
|
||||
'ask': this.safeFloat (ticker, 'l'),
|
||||
'askVolume': undefined,
|
||||
'vwap': undefined,
|
||||
'open': undefined,
|
||||
'close': last,
|
||||
'last': last,
|
||||
'previousClose': undefined,
|
||||
'change': undefined,
|
||||
'percentage': undefined,
|
||||
'average': averagePrice,
|
||||
'baseVolume': baseVolume,
|
||||
'quoteVolume': quoteVolume,
|
||||
'info': ticker,
|
||||
};
|
||||
}
|
||||
|
||||
async fetchTrades (symbol, since = undefined, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const method = this.options['fetchTradesMethod'];
|
||||
const request = {
|
||||
'pair': market['id'],
|
||||
};
|
||||
const response = await this[method] (this.extend (request, params));
|
||||
if (typeof response === 'string') {
|
||||
throw new ExchangeError (response);
|
||||
}
|
||||
return this.parseTrades (response, market, since, limit);
|
||||
}
|
||||
|
||||
async createOrder (symbol, type, side, amount, price = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
let method = 'privatePostOrderAddOrder';
|
||||
const request = {
|
||||
'Amount': amount,
|
||||
'Pair': this.marketId (symbol),
|
||||
};
|
||||
if (type === 'market') {
|
||||
method += 'MarketPrice' + this.capitalize (side);
|
||||
} else {
|
||||
request['Price'] = price;
|
||||
request['Total'] = amount * price;
|
||||
request['IsBid'] = (side === 'buy');
|
||||
}
|
||||
const response = await this[method] (this.extend (request, params));
|
||||
return {
|
||||
'info': response,
|
||||
'id': response['NewOrder']['id'],
|
||||
};
|
||||
}
|
||||
|
||||
async cancelOrder (id, symbol = undefined, params = {}) {
|
||||
const request = {
|
||||
'id': id,
|
||||
};
|
||||
return await this.privatePostOrderCancelOrder (this.extend (request, params));
|
||||
}
|
||||
|
||||
async fetchOpenOrders (symbol = undefined, since = undefined, limit = undefined, params = {}) {
|
||||
if (symbol === undefined) {
|
||||
throw new ArgumentsRequired (this.id + ' fetchOpenOrders() requires a symbol argument');
|
||||
}
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const request = {
|
||||
'pair': market['id'],
|
||||
};
|
||||
const response = await this.privateGetOrderMyOrders (this.extend (request, params));
|
||||
const orders = this.safeValue (response, market['id'], {});
|
||||
const asks = this.safeValue (orders, 'ask', []);
|
||||
const bids = this.safeValue (orders, 'bid', []);
|
||||
return this.parseOrders (this.arrayConcat (asks, bids), market, since, limit);
|
||||
}
|
||||
|
||||
parseOrder (order, market = undefined) {
|
||||
const timestamp = this.safeInteger (order, 'created');
|
||||
const price = this.safeFloat (order, 'price');
|
||||
const amount = this.safeFloat (order, 'amount');
|
||||
let cost = undefined;
|
||||
if (price !== undefined) {
|
||||
if (amount !== undefined) {
|
||||
cost = price * amount;
|
||||
}
|
||||
}
|
||||
let symbol = undefined;
|
||||
if (market !== undefined) {
|
||||
symbol = market['symbol'];
|
||||
}
|
||||
let side = this.safeValue (order, 'type');
|
||||
if (side === 0) {
|
||||
side = 'buy';
|
||||
} else if (side === 1) {
|
||||
side = 'sell';
|
||||
}
|
||||
const id = this.safeString (order, 'id');
|
||||
const status = this.safeString (order, 'status');
|
||||
return {
|
||||
'id': id,
|
||||
'clientOrderId': undefined,
|
||||
'timestamp': timestamp,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'lastTradeTimestamp': undefined,
|
||||
'status': status,
|
||||
'symbol': symbol,
|
||||
'type': undefined,
|
||||
'side': side,
|
||||
'price': price,
|
||||
'amount': amount,
|
||||
'filled': undefined,
|
||||
'remaining': undefined,
|
||||
'cost': cost,
|
||||
'trades': undefined,
|
||||
'fee': undefined,
|
||||
'info': order,
|
||||
'average': undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async fetchMyTrades (symbol = undefined, since = undefined, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
let market = undefined;
|
||||
const request = {};
|
||||
if (limit !== undefined) {
|
||||
request['take'] = limit;
|
||||
}
|
||||
request['take'] = limit;
|
||||
if (since !== undefined) {
|
||||
request['toTime'] = this.ymd (this.milliseconds (), '.');
|
||||
request['fromTime'] = this.ymd (since, '.');
|
||||
}
|
||||
if (symbol !== undefined) {
|
||||
market = this.market (symbol);
|
||||
request['pair'] = market['id'];
|
||||
}
|
||||
const response = await this.privateGetOrderOrderHistory (this.extend (request, params));
|
||||
return this.parseTrades (response, market, since, limit);
|
||||
}
|
||||
|
||||
parseTrade (trade, market = undefined) {
|
||||
let timestamp = undefined;
|
||||
let id = undefined;
|
||||
let price = undefined;
|
||||
let amount = undefined;
|
||||
let orderId = undefined;
|
||||
let feeCost = undefined;
|
||||
let side = undefined;
|
||||
const reference = this.safeString (trade, 'reference');
|
||||
if (reference !== undefined) {
|
||||
timestamp = this.safeTimestamp (trade, 'ticks');
|
||||
price = this.safeFloat (trade, 'price');
|
||||
amount = this.safeFloat (trade, 'firstAmount');
|
||||
const reference_parts = reference.split ('|'); // reference contains 'pair|orderId|tradeId'
|
||||
if (market === undefined) {
|
||||
const marketId = this.safeString (trade, 'pair');
|
||||
if (marketId in this.markets_by_id[marketId]) {
|
||||
market = this.markets_by_id[marketId];
|
||||
} else if (reference_parts[0] in this.markets_by_id) {
|
||||
market = this.markets_by_id[reference_parts[0]];
|
||||
}
|
||||
}
|
||||
orderId = reference_parts[1];
|
||||
id = reference_parts[2];
|
||||
side = this.safeInteger (trade, 'action');
|
||||
if (side === 0) {
|
||||
side = 'buy';
|
||||
} else if (side === 1) {
|
||||
side = 'sell';
|
||||
}
|
||||
feeCost = this.safeFloat (trade, 'feeAmount');
|
||||
} else {
|
||||
timestamp = this.safeTimestamp (trade, 'date');
|
||||
id = this.safeString (trade, 'tid');
|
||||
price = this.safeFloat (trade, 'price');
|
||||
amount = this.safeFloat (trade, 'amount');
|
||||
side = this.safeValue (trade, 'isBid');
|
||||
if (side !== undefined) {
|
||||
if (side) {
|
||||
side = 'buy';
|
||||
} else {
|
||||
side = 'sell';
|
||||
}
|
||||
}
|
||||
}
|
||||
let symbol = undefined;
|
||||
if (market !== undefined) {
|
||||
symbol = market['symbol'];
|
||||
}
|
||||
return {
|
||||
'info': trade,
|
||||
'id': id,
|
||||
'timestamp': timestamp,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'symbol': symbol,
|
||||
'order': orderId,
|
||||
'type': undefined,
|
||||
'side': side,
|
||||
'takerOrMaker': undefined,
|
||||
'price': price,
|
||||
'amount': amount,
|
||||
'cost': price * amount,
|
||||
'fee': {
|
||||
'cost': feeCost,
|
||||
'currency': 'NIS',
|
||||
'rate': undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
sign (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) {
|
||||
let url = this.urls['api'] + '/' + this.implodeParams (path, params);
|
||||
if (api === 'public') {
|
||||
url += '.json';
|
||||
} else {
|
||||
this.checkRequiredCredentials ();
|
||||
const nonce = this.nonce ();
|
||||
const query = this.extend ({
|
||||
'nonce': nonce,
|
||||
}, params);
|
||||
const auth = this.urlencode (query);
|
||||
if (method === 'GET') {
|
||||
if (Object.keys (query).length) {
|
||||
url += '?' + auth;
|
||||
}
|
||||
} else {
|
||||
body = auth;
|
||||
}
|
||||
const signature = this.hmac (this.encode (auth), this.encode (this.secret), 'sha512', 'base64');
|
||||
headers = {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'key': this.apiKey,
|
||||
'sign': this.decode (signature),
|
||||
};
|
||||
}
|
||||
return { 'url': url, 'method': method, 'body': body, 'headers': headers };
|
||||
}
|
||||
};
|
||||
|
||||
+567
@@ -0,0 +1,567 @@
|
||||
'use strict';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const Exchange = require ('./base/Exchange');
|
||||
const { ExchangeError, AuthenticationError, InvalidNonce, InsufficientFunds, InvalidOrder, OrderNotFound, PermissionDenied } = require ('./base/errors');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
module.exports = class bitbank extends Exchange {
|
||||
describe () {
|
||||
return this.deepExtend (super.describe (), {
|
||||
'id': 'bitbank',
|
||||
'name': 'bitbank',
|
||||
'countries': [ 'JP' ],
|
||||
'version': 'v1',
|
||||
'has': {
|
||||
'fetchOHLCV': true,
|
||||
'fetchOpenOrders': true,
|
||||
'fetchMyTrades': true,
|
||||
'fetchDepositAddress': true,
|
||||
'withdraw': true,
|
||||
},
|
||||
'timeframes': {
|
||||
'1m': '1min',
|
||||
'5m': '5min',
|
||||
'15m': '15min',
|
||||
'30m': '30min',
|
||||
'1h': '1hour',
|
||||
'4h': '4hour',
|
||||
'8h': '8hour',
|
||||
'12h': '12hour',
|
||||
'1d': '1day',
|
||||
'1w': '1week',
|
||||
},
|
||||
'urls': {
|
||||
'logo': 'https://user-images.githubusercontent.com/1294454/37808081-b87f2d9c-2e59-11e8-894d-c1900b7584fe.jpg',
|
||||
'api': {
|
||||
'public': 'https://public.bitbank.cc',
|
||||
'private': 'https://api.bitbank.cc',
|
||||
},
|
||||
'www': 'https://bitbank.cc/',
|
||||
'doc': 'https://docs.bitbank.cc/',
|
||||
'fees': 'https://bitbank.cc/docs/fees/',
|
||||
},
|
||||
'api': {
|
||||
'public': {
|
||||
'get': [
|
||||
'{pair}/ticker',
|
||||
'{pair}/depth',
|
||||
'{pair}/transactions',
|
||||
'{pair}/transactions/{yyyymmdd}',
|
||||
'{pair}/candlestick/{candletype}/{yyyymmdd}',
|
||||
],
|
||||
},
|
||||
'private': {
|
||||
'get': [
|
||||
'user/assets',
|
||||
'user/spot/order',
|
||||
'user/spot/active_orders',
|
||||
'user/spot/trade_history',
|
||||
'user/withdrawal_account',
|
||||
],
|
||||
'post': [
|
||||
'user/spot/order',
|
||||
'user/spot/cancel_order',
|
||||
'user/spot/cancel_orders',
|
||||
'user/spot/orders_info',
|
||||
'user/request_withdrawal',
|
||||
],
|
||||
},
|
||||
},
|
||||
'markets': {
|
||||
'BCH/BTC': { 'id': 'bcc_btc', 'symbol': 'BCH/BTC', 'base': 'BCH', 'quote': 'BTC', 'baseId': 'bcc', 'quoteId': 'btc' },
|
||||
'BCH/JPY': { 'id': 'bcc_jpy', 'symbol': 'BCH/JPY', 'base': 'BCH', 'quote': 'JPY', 'baseId': 'bcc', 'quoteId': 'jpy' },
|
||||
'MONA/BTC': { 'id': 'mona_btc', 'symbol': 'MONA/BTC', 'base': 'MONA', 'quote': 'BTC', 'baseId': 'mona', 'quoteId': 'btc' },
|
||||
'MONA/JPY': { 'id': 'mona_jpy', 'symbol': 'MONA/JPY', 'base': 'MONA', 'quote': 'JPY', 'baseId': 'mona', 'quoteId': 'jpy' },
|
||||
'ETH/BTC': { 'id': 'eth_btc', 'symbol': 'ETH/BTC', 'base': 'ETH', 'quote': 'BTC', 'baseId': 'eth', 'quoteId': 'btc' },
|
||||
'LTC/BTC': { 'id': 'ltc_btc', 'symbol': 'LTC/BTC', 'base': 'LTC', 'quote': 'BTC', 'baseId': 'ltc', 'quoteId': 'btc' },
|
||||
'XRP/JPY': { 'id': 'xrp_jpy', 'symbol': 'XRP/JPY', 'base': 'XRP', 'quote': 'JPY', 'baseId': 'xrp', 'quoteId': 'jpy' },
|
||||
'BTC/JPY': { 'id': 'btc_jpy', 'symbol': 'BTC/JPY', 'base': 'BTC', 'quote': 'JPY', 'baseId': 'btc', 'quoteId': 'jpy' },
|
||||
},
|
||||
'fees': {
|
||||
'trading': {
|
||||
'maker': -0.02 / 100,
|
||||
'taker': 0.12 / 100,
|
||||
},
|
||||
'funding': {
|
||||
'withdraw': {
|
||||
// 'JPY': (amount > 30000) ? 756 : 540,
|
||||
'BTC': 0.001,
|
||||
'LTC': 0.001,
|
||||
'XRP': 0.15,
|
||||
'ETH': 0.0005,
|
||||
'MONA': 0.001,
|
||||
'BCC': 0.001,
|
||||
},
|
||||
},
|
||||
},
|
||||
'precision': {
|
||||
'price': 8,
|
||||
'amount': 8,
|
||||
},
|
||||
'exceptions': {
|
||||
'20001': AuthenticationError,
|
||||
'20002': AuthenticationError,
|
||||
'20003': AuthenticationError,
|
||||
'20005': AuthenticationError,
|
||||
'20004': InvalidNonce,
|
||||
'40020': InvalidOrder,
|
||||
'40021': InvalidOrder,
|
||||
'40025': ExchangeError,
|
||||
'40013': OrderNotFound,
|
||||
'40014': OrderNotFound,
|
||||
'50008': PermissionDenied,
|
||||
'50009': OrderNotFound,
|
||||
'50010': OrderNotFound,
|
||||
'60001': InsufficientFunds,
|
||||
'60005': InvalidOrder,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
parseTicker (ticker, market = undefined) {
|
||||
let symbol = undefined;
|
||||
if (market !== undefined) {
|
||||
symbol = market['symbol'];
|
||||
}
|
||||
const timestamp = this.safeInteger (ticker, 'timestamp');
|
||||
const last = this.safeFloat (ticker, 'last');
|
||||
return {
|
||||
'symbol': symbol,
|
||||
'timestamp': timestamp,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'high': this.safeFloat (ticker, 'high'),
|
||||
'low': this.safeFloat (ticker, 'low'),
|
||||
'bid': this.safeFloat (ticker, 'buy'),
|
||||
'bidVolume': undefined,
|
||||
'ask': this.safeFloat (ticker, 'sell'),
|
||||
'askVolume': undefined,
|
||||
'vwap': undefined,
|
||||
'open': undefined,
|
||||
'close': last,
|
||||
'last': last,
|
||||
'previousClose': undefined,
|
||||
'change': undefined,
|
||||
'percentage': undefined,
|
||||
'average': undefined,
|
||||
'baseVolume': this.safeFloat (ticker, 'vol'),
|
||||
'quoteVolume': undefined,
|
||||
'info': ticker,
|
||||
};
|
||||
}
|
||||
|
||||
async fetchTicker (symbol, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const request = {
|
||||
'pair': market['id'],
|
||||
};
|
||||
const response = await this.publicGetPairTicker (this.extend (request, params));
|
||||
return this.parseTicker (response['data'], market);
|
||||
}
|
||||
|
||||
async fetchOrderBook (symbol, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const request = {
|
||||
'pair': this.marketId (symbol),
|
||||
};
|
||||
const response = await this.publicGetPairDepth (this.extend (request, params));
|
||||
const orderbook = this.safeValue (response, 'data', {});
|
||||
const timestamp = this.safeInteger (orderbook, 'timestamp');
|
||||
return this.parseOrderBook (orderbook, timestamp);
|
||||
}
|
||||
|
||||
parseTrade (trade, market = undefined) {
|
||||
const timestamp = this.safeInteger (trade, 'executed_at');
|
||||
let symbol = undefined;
|
||||
let feeCurrency = undefined;
|
||||
if (market !== undefined) {
|
||||
symbol = market['symbol'];
|
||||
feeCurrency = market['quote'];
|
||||
}
|
||||
const price = this.safeFloat (trade, 'price');
|
||||
const amount = this.safeFloat (trade, 'amount');
|
||||
let cost = undefined;
|
||||
if (price !== undefined) {
|
||||
if (amount !== undefined) {
|
||||
cost = parseFloat (this.costToPrecision (symbol, price * amount));
|
||||
}
|
||||
}
|
||||
const id = this.safeString2 (trade, 'transaction_id', 'trade_id');
|
||||
const takerOrMaker = this.safeString (trade, 'maker_taker');
|
||||
let fee = undefined;
|
||||
const feeCost = this.safeFloat (trade, 'fee_amount_quote');
|
||||
if (feeCost !== undefined) {
|
||||
fee = {
|
||||
'currency': market['quote'],
|
||||
'cost': feeCurrency,
|
||||
};
|
||||
}
|
||||
const orderId = this.safeString (trade, 'order_id');
|
||||
const type = this.safeString (trade, 'type');
|
||||
const side = this.safeString (trade, 'side');
|
||||
return {
|
||||
'timestamp': timestamp,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'symbol': symbol,
|
||||
'id': id,
|
||||
'order': orderId,
|
||||
'type': type,
|
||||
'side': side,
|
||||
'takerOrMaker': takerOrMaker,
|
||||
'price': price,
|
||||
'amount': amount,
|
||||
'cost': cost,
|
||||
'fee': fee,
|
||||
'info': trade,
|
||||
};
|
||||
}
|
||||
|
||||
async fetchTrades (symbol, since = undefined, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const request = {
|
||||
'pair': market['id'],
|
||||
};
|
||||
const response = await this.publicGetPairTransactions (this.extend (request, params));
|
||||
return this.parseTrades (response['data']['transactions'], market, since, limit);
|
||||
}
|
||||
|
||||
parseOHLCV (ohlcv, market = undefined, timeframe = '5m', since = undefined, limit = undefined) {
|
||||
return [
|
||||
ohlcv[5],
|
||||
parseFloat (ohlcv[0]),
|
||||
parseFloat (ohlcv[1]),
|
||||
parseFloat (ohlcv[2]),
|
||||
parseFloat (ohlcv[3]),
|
||||
parseFloat (ohlcv[4]),
|
||||
];
|
||||
}
|
||||
|
||||
async fetchOHLCV (symbol, timeframe = '5m', since = undefined, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
let date = this.milliseconds ();
|
||||
date = this.ymd (date);
|
||||
date = date.split ('-');
|
||||
const request = {
|
||||
'pair': market['id'],
|
||||
'candletype': this.timeframes[timeframe],
|
||||
'yyyymmdd': date.join (''),
|
||||
};
|
||||
const response = await this.publicGetPairCandlestickCandletypeYyyymmdd (this.extend (request, params));
|
||||
const ohlcv = this.safeValue (response['data']['candlestick'][0], 'ohlcv');
|
||||
return this.parseOHLCVs (ohlcv, market, timeframe, since, limit);
|
||||
}
|
||||
|
||||
async fetchBalance (params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const response = await this.privateGetUserAssets (params);
|
||||
const result = { 'info': response };
|
||||
const balances = response['data']['assets'];
|
||||
for (let i = 0; i < balances.length; i++) {
|
||||
const balance = balances[i];
|
||||
const currencyId = this.safeString (balance, 'asset');
|
||||
const code = this.safeCurrencyCode (currencyId);
|
||||
const account = {
|
||||
'free': this.safeFloat (balance, 'free_amount'),
|
||||
'used': this.safeFloat (balance, 'locked_amount'),
|
||||
'total': this.safeFloat (balance, 'onhand_amount'),
|
||||
};
|
||||
result[code] = account;
|
||||
}
|
||||
return this.parseBalance (result);
|
||||
}
|
||||
|
||||
parseOrderStatus (status) {
|
||||
const statuses = {
|
||||
'UNFILLED': 'open',
|
||||
'PARTIALLY_FILLED': 'open',
|
||||
'FULLY_FILLED': 'closed',
|
||||
'CANCELED_UNFILLED': 'canceled',
|
||||
'CANCELED_PARTIALLY_FILLED': 'canceled',
|
||||
};
|
||||
return this.safeString (statuses, status, status);
|
||||
}
|
||||
|
||||
parseOrder (order, market = undefined) {
|
||||
const id = this.safeString (order, 'order_id');
|
||||
const marketId = this.safeString (order, 'pair');
|
||||
let symbol = undefined;
|
||||
if (marketId && !market && (marketId in this.marketsById)) {
|
||||
market = this.marketsById[marketId];
|
||||
}
|
||||
if (market !== undefined) {
|
||||
symbol = market['symbol'];
|
||||
}
|
||||
const timestamp = this.safeInteger (order, 'ordered_at');
|
||||
const price = this.safeFloat (order, 'price');
|
||||
const amount = this.safeFloat (order, 'start_amount');
|
||||
const filled = this.safeFloat (order, 'executed_amount');
|
||||
const remaining = this.safeFloat (order, 'remaining_amount');
|
||||
const average = this.safeFloat (order, 'average_price');
|
||||
let cost = undefined;
|
||||
if (filled !== undefined) {
|
||||
if (average !== undefined) {
|
||||
cost = filled * average;
|
||||
}
|
||||
}
|
||||
const status = this.parseOrderStatus (this.safeString (order, 'status'));
|
||||
const type = this.safeStringLower (order, 'type');
|
||||
const side = this.safeStringLower (order, 'side');
|
||||
return {
|
||||
'id': id,
|
||||
'clientOrderId': undefined,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'timestamp': timestamp,
|
||||
'lastTradeTimestamp': undefined,
|
||||
'status': status,
|
||||
'symbol': symbol,
|
||||
'type': type,
|
||||
'side': side,
|
||||
'price': price,
|
||||
'cost': cost,
|
||||
'average': average,
|
||||
'amount': amount,
|
||||
'filled': filled,
|
||||
'remaining': remaining,
|
||||
'trades': undefined,
|
||||
'fee': undefined,
|
||||
'info': order,
|
||||
};
|
||||
}
|
||||
|
||||
async createOrder (symbol, type, side, amount, price = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
if (price === undefined) {
|
||||
throw new InvalidOrder (this.id + ' createOrder requires a price argument for both market and limit orders');
|
||||
}
|
||||
const request = {
|
||||
'pair': market['id'],
|
||||
'amount': this.amountToPrecision (symbol, amount),
|
||||
'price': this.priceToPrecision (symbol, price),
|
||||
'side': side,
|
||||
'type': type,
|
||||
};
|
||||
const response = await this.privatePostUserSpotOrder (this.extend (request, params));
|
||||
const order = this.parseOrder (response['data'], market);
|
||||
const id = order['id'];
|
||||
this.orders[id] = order;
|
||||
return order;
|
||||
}
|
||||
|
||||
async cancelOrder (id, symbol = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const request = {
|
||||
'order_id': id,
|
||||
'pair': market['id'],
|
||||
};
|
||||
const response = await this.privatePostUserSpotCancelOrder (this.extend (request, params));
|
||||
return response['data'];
|
||||
}
|
||||
|
||||
async fetchOrder (id, symbol = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const request = {
|
||||
'order_id': id,
|
||||
'pair': market['id'],
|
||||
};
|
||||
const response = await this.privateGetUserSpotOrder (this.extend (request, params));
|
||||
return this.parseOrder (response['data']);
|
||||
}
|
||||
|
||||
async fetchOpenOrders (symbol = undefined, since = undefined, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const request = {
|
||||
'pair': market['id'],
|
||||
};
|
||||
if (limit !== undefined) {
|
||||
request['count'] = limit;
|
||||
}
|
||||
if (since !== undefined) {
|
||||
request['since'] = parseInt (since / 1000);
|
||||
}
|
||||
const response = await this.privateGetUserSpotActiveOrders (this.extend (request, params));
|
||||
return this.parseOrders (response['data']['orders'], market, since, limit);
|
||||
}
|
||||
|
||||
async fetchMyTrades (symbol = undefined, since = undefined, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
let market = undefined;
|
||||
if (symbol !== undefined) {
|
||||
market = this.market (symbol);
|
||||
}
|
||||
const request = {};
|
||||
if (market !== undefined) {
|
||||
request['pair'] = market['id'];
|
||||
}
|
||||
if (limit !== undefined) {
|
||||
request['count'] = limit;
|
||||
}
|
||||
if (since !== undefined) {
|
||||
request['since'] = parseInt (since / 1000);
|
||||
}
|
||||
const response = await this.privateGetUserSpotTradeHistory (this.extend (request, params));
|
||||
return this.parseTrades (response['data']['trades'], market, since, limit);
|
||||
}
|
||||
|
||||
async fetchDepositAddress (code, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const currency = this.currency (code);
|
||||
const request = {
|
||||
'asset': currency['id'],
|
||||
};
|
||||
const response = await this.privateGetUserWithdrawalAccount (this.extend (request, params));
|
||||
// Not sure about this if there could be more than one account...
|
||||
const accounts = response['data']['accounts'];
|
||||
const address = this.safeString (accounts[0], 'address');
|
||||
return {
|
||||
'currency': currency,
|
||||
'address': address,
|
||||
'tag': undefined,
|
||||
'info': response,
|
||||
};
|
||||
}
|
||||
|
||||
async withdraw (code, amount, address, tag = undefined, params = {}) {
|
||||
if (!('uuid' in params)) {
|
||||
throw new ExchangeError (this.id + ' uuid is required for withdrawal');
|
||||
}
|
||||
await this.loadMarkets ();
|
||||
const currency = this.currency (code);
|
||||
const request = {
|
||||
'asset': currency['id'],
|
||||
'amount': amount,
|
||||
};
|
||||
const response = await this.privatePostUserRequestWithdrawal (this.extend (request, params));
|
||||
const txid = this.safeString (response['data'], 'txid');
|
||||
return {
|
||||
'info': response,
|
||||
'id': txid,
|
||||
};
|
||||
}
|
||||
|
||||
nonce () {
|
||||
return this.milliseconds ();
|
||||
}
|
||||
|
||||
sign (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) {
|
||||
let query = this.omit (params, this.extractParams (path));
|
||||
let url = this.urls['api'][api] + '/';
|
||||
if (api === 'public') {
|
||||
url += this.implodeParams (path, params);
|
||||
if (Object.keys (query).length) {
|
||||
url += '?' + this.urlencode (query);
|
||||
}
|
||||
} else {
|
||||
this.checkRequiredCredentials ();
|
||||
const nonce = this.nonce ().toString ();
|
||||
let auth = nonce;
|
||||
url += this.version + '/' + this.implodeParams (path, params);
|
||||
if (method === 'POST') {
|
||||
body = this.json (query);
|
||||
auth += body;
|
||||
} else {
|
||||
auth += '/' + this.version + '/' + path;
|
||||
if (Object.keys (query).length) {
|
||||
query = this.urlencode (query);
|
||||
url += '?' + query;
|
||||
auth += '?' + query;
|
||||
}
|
||||
}
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'ACCESS-KEY': this.apiKey,
|
||||
'ACCESS-NONCE': nonce,
|
||||
'ACCESS-SIGNATURE': this.hmac (this.encode (auth), this.encode (this.secret)),
|
||||
};
|
||||
}
|
||||
return { 'url': url, 'method': method, 'body': body, 'headers': headers };
|
||||
}
|
||||
|
||||
async request (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) {
|
||||
const response = await this.fetch2 (path, api, method, params, headers, body);
|
||||
const success = this.safeInteger (response, 'success');
|
||||
const data = this.safeValue (response, 'data');
|
||||
if (!success || !data) {
|
||||
const errorMessages = {
|
||||
'10000': 'URL does not exist',
|
||||
'10001': 'A system error occurred. Please contact support',
|
||||
'10002': 'Invalid JSON format. Please check the contents of transmission',
|
||||
'10003': 'A system error occurred. Please contact support',
|
||||
'10005': 'A timeout error occurred. Please wait for a while and try again',
|
||||
'20001': 'API authentication failed',
|
||||
'20002': 'Illegal API key',
|
||||
'20003': 'API key does not exist',
|
||||
'20004': 'API Nonce does not exist',
|
||||
'20005': 'API signature does not exist',
|
||||
'20011': 'Two-step verification failed',
|
||||
'20014': 'SMS authentication failed',
|
||||
'30001': 'Please specify the order quantity',
|
||||
'30006': 'Please specify the order ID',
|
||||
'30007': 'Please specify the order ID array',
|
||||
'30009': 'Please specify the stock',
|
||||
'30012': 'Please specify the order price',
|
||||
'30013': 'Trade Please specify either',
|
||||
'30015': 'Please specify the order type',
|
||||
'30016': 'Please specify asset name',
|
||||
'30019': 'Please specify uuid',
|
||||
'30039': 'Please specify the amount to be withdrawn',
|
||||
'40001': 'The order quantity is invalid',
|
||||
'40006': 'Count value is invalid',
|
||||
'40007': 'End time is invalid',
|
||||
'40008': 'end_id Value is invalid',
|
||||
'40009': 'The from_id value is invalid',
|
||||
'40013': 'The order ID is invalid',
|
||||
'40014': 'The order ID array is invalid',
|
||||
'40015': 'Too many specified orders',
|
||||
'40017': 'Incorrect issue name',
|
||||
'40020': 'The order price is invalid',
|
||||
'40021': 'The trading classification is invalid',
|
||||
'40022': 'Start date is invalid',
|
||||
'40024': 'The order type is invalid',
|
||||
'40025': 'Incorrect asset name',
|
||||
'40028': 'uuid is invalid',
|
||||
'40048': 'The amount of withdrawal is illegal',
|
||||
'50003': 'Currently, this account is in a state where you can not perform the operation you specified. Please contact support',
|
||||
'50004': 'Currently, this account is temporarily registered. Please try again after registering your account',
|
||||
'50005': 'Currently, this account is locked. Please contact support',
|
||||
'50006': 'Currently, this account is locked. Please contact support',
|
||||
'50008': 'User identification has not been completed',
|
||||
'50009': 'Your order does not exist',
|
||||
'50010': 'Can not cancel specified order',
|
||||
'50011': 'API not found',
|
||||
'60001': 'The number of possessions is insufficient',
|
||||
'60002': 'It exceeds the quantity upper limit of the tender buying order',
|
||||
'60003': 'The specified quantity exceeds the limit',
|
||||
'60004': 'The specified quantity is below the threshold',
|
||||
'60005': 'The specified price is above the limit',
|
||||
'60006': 'The specified price is below the lower limit',
|
||||
'70001': 'A system error occurred. Please contact support',
|
||||
'70002': 'A system error occurred. Please contact support',
|
||||
'70003': 'A system error occurred. Please contact support',
|
||||
'70004': 'We are unable to accept orders as the transaction is currently suspended',
|
||||
'70005': 'Order can not be accepted because purchase order is currently suspended',
|
||||
'70006': 'We can not accept orders because we are currently unsubscribed ',
|
||||
'70009': 'We are currently temporarily restricting orders to be carried out. Please use the limit order.',
|
||||
'70010': 'We are temporarily raising the minimum order quantity as the system load is now rising.',
|
||||
};
|
||||
const errorClasses = this.exceptions;
|
||||
const code = this.safeString (data, 'code');
|
||||
const message = this.safeString (errorMessages, code, 'Error');
|
||||
const ErrorClass = this.safeValue (errorClasses, code);
|
||||
if (ErrorClass !== undefined) {
|
||||
throw new ErrorClass (message);
|
||||
} else {
|
||||
throw new ExchangeError (this.id + ' ' + this.json (response));
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
};
|
||||
+1279
File diff suppressed because it is too large.
Load diff
+1183
File diff suppressed because it is too large.
Load diff
+1193
File diff suppressed because it is too large.
Load diff
+490
@@ -0,0 +1,490 @@
|
||||
'use strict';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const Exchange = require ('./base/Exchange');
|
||||
const { ExchangeError, ArgumentsRequired, OrderNotFound } = require ('./base/errors');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
module.exports = class bitflyer extends Exchange {
|
||||
describe () {
|
||||
return this.deepExtend (super.describe (), {
|
||||
'id': 'bitflyer',
|
||||
'name': 'bitFlyer',
|
||||
'countries': [ 'JP' ],
|
||||
'version': 'v1',
|
||||
'rateLimit': 1000, // their nonce-timestamp is in seconds...
|
||||
'has': {
|
||||
'CORS': false,
|
||||
'withdraw': true,
|
||||
'fetchMyTrades': true,
|
||||
'fetchOrders': true,
|
||||
'fetchOrder': 'emulated',
|
||||
'fetchOpenOrders': 'emulated',
|
||||
'fetchClosedOrders': 'emulated',
|
||||
},
|
||||
'urls': {
|
||||
'logo': 'https://user-images.githubusercontent.com/1294454/28051642-56154182-660e-11e7-9b0d-6042d1e6edd8.jpg',
|
||||
'api': 'https://api.bitflyer.jp',
|
||||
'www': 'https://bitflyer.jp',
|
||||
'doc': 'https://lightning.bitflyer.com/docs?lang=en',
|
||||
},
|
||||
'api': {
|
||||
'public': {
|
||||
'get': [
|
||||
'getmarkets/usa', // new (wip)
|
||||
'getmarkets/eu', // new (wip)
|
||||
'getmarkets', // or 'markets'
|
||||
'getboard', // ...
|
||||
'getticker',
|
||||
'getexecutions',
|
||||
'gethealth',
|
||||
'getboardstate',
|
||||
'getchats',
|
||||
],
|
||||
},
|
||||
'private': {
|
||||
'get': [
|
||||
'getpermissions',
|
||||
'getbalance',
|
||||
'getbalancehistory',
|
||||
'getcollateral',
|
||||
'getcollateralhistory',
|
||||
'getcollateralaccounts',
|
||||
'getaddresses',
|
||||
'getcoinins',
|
||||
'getcoinouts',
|
||||
'getbankaccounts',
|
||||
'getdeposits',
|
||||
'getwithdrawals',
|
||||
'getchildorders',
|
||||
'getparentorders',
|
||||
'getparentorder',
|
||||
'getexecutions',
|
||||
'getpositions',
|
||||
'gettradingcommission',
|
||||
],
|
||||
'post': [
|
||||
'sendcoin',
|
||||
'withdraw',
|
||||
'sendchildorder',
|
||||
'cancelchildorder',
|
||||
'sendparentorder',
|
||||
'cancelparentorder',
|
||||
'cancelallchildorders',
|
||||
],
|
||||
},
|
||||
},
|
||||
'fees': {
|
||||
'trading': {
|
||||
'maker': 0.2 / 100,
|
||||
'taker': 0.2 / 100,
|
||||
},
|
||||
'BTC/JPY': {
|
||||
'maker': 0.15 / 100,
|
||||
'taker': 0.15 / 100,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async fetchMarkets (params = {}) {
|
||||
const jp_markets = await this.publicGetGetmarkets (params);
|
||||
const us_markets = await this.publicGetGetmarketsUsa (params);
|
||||
const eu_markets = await this.publicGetGetmarketsEu (params);
|
||||
let markets = this.arrayConcat (jp_markets, us_markets);
|
||||
markets = this.arrayConcat (markets, eu_markets);
|
||||
const result = [];
|
||||
for (let i = 0; i < markets.length; i++) {
|
||||
const market = markets[i];
|
||||
const id = this.safeString (market, 'product_code');
|
||||
const currencies = id.split ('_');
|
||||
let baseId = undefined;
|
||||
let quoteId = undefined;
|
||||
let base = undefined;
|
||||
let quote = undefined;
|
||||
const numCurrencies = currencies.length;
|
||||
if (numCurrencies === 1) {
|
||||
baseId = id.slice (0, 3);
|
||||
quoteId = id.slice (3, 6);
|
||||
} else if (numCurrencies === 2) {
|
||||
baseId = currencies[0];
|
||||
quoteId = currencies[1];
|
||||
} else {
|
||||
baseId = currencies[1];
|
||||
quoteId = currencies[2];
|
||||
}
|
||||
base = this.safeCurrencyCode (baseId);
|
||||
quote = this.safeCurrencyCode (quoteId);
|
||||
const symbol = (numCurrencies === 2) ? (base + '/' + quote) : id;
|
||||
const fees = this.safeValue (this.fees, symbol, this.fees['trading']);
|
||||
let maker = this.safeValue (fees, 'maker', this.fees['trading']['maker']);
|
||||
let taker = this.safeValue (fees, 'taker', this.fees['trading']['taker']);
|
||||
let spot = true;
|
||||
let future = false;
|
||||
let type = 'spot';
|
||||
if (('alias' in market) || (currencies[0] === 'FX')) {
|
||||
type = 'future';
|
||||
future = true;
|
||||
spot = false;
|
||||
maker = 0.0;
|
||||
taker = 0.0;
|
||||
}
|
||||
result.push ({
|
||||
'id': id,
|
||||
'symbol': symbol,
|
||||
'base': base,
|
||||
'quote': quote,
|
||||
'baseId': baseId,
|
||||
'quoteId': quoteId,
|
||||
'maker': maker,
|
||||
'taker': taker,
|
||||
'type': type,
|
||||
'spot': spot,
|
||||
'future': future,
|
||||
'info': market,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async fetchBalance (params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const response = await this.privateGetGetbalance (params);
|
||||
//
|
||||
// [
|
||||
// {
|
||||
// "currency_code": "JPY",
|
||||
// "amount": 1024078,
|
||||
// "available": 508000
|
||||
// },
|
||||
// {
|
||||
// "currency_code": "BTC",
|
||||
// "amount": 10.24,
|
||||
// "available": 4.12
|
||||
// },
|
||||
// {
|
||||
// "currency_code": "ETH",
|
||||
// "amount": 20.48,
|
||||
// "available": 16.38
|
||||
// }
|
||||
// ]
|
||||
//
|
||||
const result = { 'info': response };
|
||||
for (let i = 0; i < response.length; i++) {
|
||||
const balance = response[i];
|
||||
const currencyId = this.safeString (balance, 'currency_code');
|
||||
const code = this.safeCurrencyCode (currencyId);
|
||||
const account = this.account ();
|
||||
account['total'] = this.safeFloat (balance, 'amount');
|
||||
account['free'] = this.safeFloat (balance, 'available');
|
||||
result[code] = account;
|
||||
}
|
||||
return this.parseBalance (result);
|
||||
}
|
||||
|
||||
async fetchOrderBook (symbol, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const request = {
|
||||
'product_code': this.marketId (symbol),
|
||||
};
|
||||
const orderbook = await this.publicGetGetboard (this.extend (request, params));
|
||||
return this.parseOrderBook (orderbook, undefined, 'bids', 'asks', 'price', 'size');
|
||||
}
|
||||
|
||||
async fetchTicker (symbol, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const request = {
|
||||
'product_code': this.marketId (symbol),
|
||||
};
|
||||
const ticker = await this.publicGetGetticker (this.extend (request, params));
|
||||
const timestamp = this.parse8601 (this.safeString (ticker, 'timestamp'));
|
||||
const last = this.safeFloat (ticker, 'ltp');
|
||||
return {
|
||||
'symbol': symbol,
|
||||
'timestamp': timestamp,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'high': undefined,
|
||||
'low': undefined,
|
||||
'bid': this.safeFloat (ticker, 'best_bid'),
|
||||
'bidVolume': undefined,
|
||||
'ask': this.safeFloat (ticker, 'best_ask'),
|
||||
'askVolume': undefined,
|
||||
'vwap': undefined,
|
||||
'open': undefined,
|
||||
'close': last,
|
||||
'last': last,
|
||||
'previousClose': undefined,
|
||||
'change': undefined,
|
||||
'percentage': undefined,
|
||||
'average': undefined,
|
||||
'baseVolume': this.safeFloat (ticker, 'volume_by_product'),
|
||||
'quoteVolume': undefined,
|
||||
'info': ticker,
|
||||
};
|
||||
}
|
||||
|
||||
parseTrade (trade, market = undefined) {
|
||||
let side = this.safeStringLower (trade, 'side');
|
||||
if (side !== undefined) {
|
||||
if (side.length < 1) {
|
||||
side = undefined;
|
||||
}
|
||||
}
|
||||
let order = undefined;
|
||||
if (side !== undefined) {
|
||||
const id = side + '_child_order_acceptance_id';
|
||||
if (id in trade) {
|
||||
order = trade[id];
|
||||
}
|
||||
}
|
||||
if (order === undefined) {
|
||||
order = this.safeString (trade, 'child_order_acceptance_id');
|
||||
}
|
||||
const timestamp = this.parse8601 (this.safeString (trade, 'exec_date'));
|
||||
const price = this.safeFloat (trade, 'price');
|
||||
const amount = this.safeFloat (trade, 'size');
|
||||
let cost = undefined;
|
||||
if (amount !== undefined) {
|
||||
if (price !== undefined) {
|
||||
cost = price * amount;
|
||||
}
|
||||
}
|
||||
const id = this.safeString (trade, 'id');
|
||||
let symbol = undefined;
|
||||
if (market !== undefined) {
|
||||
symbol = market['symbol'];
|
||||
}
|
||||
return {
|
||||
'id': id,
|
||||
'info': trade,
|
||||
'timestamp': timestamp,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'symbol': symbol,
|
||||
'order': order,
|
||||
'type': undefined,
|
||||
'side': side,
|
||||
'takerOrMaker': undefined,
|
||||
'price': price,
|
||||
'amount': amount,
|
||||
'cost': cost,
|
||||
'fee': undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async fetchTrades (symbol, since = undefined, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const request = {
|
||||
'product_code': market['id'],
|
||||
};
|
||||
const response = await this.publicGetGetexecutions (this.extend (request, params));
|
||||
return this.parseTrades (response, market, since, limit);
|
||||
}
|
||||
|
||||
async createOrder (symbol, type, side, amount, price = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const request = {
|
||||
'product_code': this.marketId (symbol),
|
||||
'child_order_type': type.toUpperCase (),
|
||||
'side': side.toUpperCase (),
|
||||
'price': price,
|
||||
'size': amount,
|
||||
};
|
||||
const result = await this.privatePostSendchildorder (this.extend (request, params));
|
||||
// { "status": - 200, "error_message": "Insufficient funds", "data": null }
|
||||
const id = this.safeString (result, 'child_order_acceptance_id');
|
||||
return {
|
||||
'info': result,
|
||||
'id': id,
|
||||
};
|
||||
}
|
||||
|
||||
async cancelOrder (id, symbol = undefined, params = {}) {
|
||||
if (symbol === undefined) {
|
||||
throw new ArgumentsRequired (this.id + ' cancelOrder() requires a `symbol` argument');
|
||||
}
|
||||
await this.loadMarkets ();
|
||||
const request = {
|
||||
'product_code': this.marketId (symbol),
|
||||
'child_order_acceptance_id': id,
|
||||
};
|
||||
return await this.privatePostCancelchildorder (this.extend (request, params));
|
||||
}
|
||||
|
||||
parseOrderStatus (status) {
|
||||
const statuses = {
|
||||
'ACTIVE': 'open',
|
||||
'COMPLETED': 'closed',
|
||||
'CANCELED': 'canceled',
|
||||
'EXPIRED': 'canceled',
|
||||
'REJECTED': 'canceled',
|
||||
};
|
||||
return this.safeString (statuses, status, status);
|
||||
}
|
||||
|
||||
parseOrder (order, market = undefined) {
|
||||
const timestamp = this.parse8601 (this.safeString (order, 'child_order_date'));
|
||||
const amount = this.safeFloat (order, 'size');
|
||||
const remaining = this.safeFloat (order, 'outstanding_size');
|
||||
const filled = this.safeFloat (order, 'executed_size');
|
||||
const price = this.safeFloat (order, 'price');
|
||||
const cost = price * filled;
|
||||
const status = this.parseOrderStatus (this.safeString (order, 'child_order_state'));
|
||||
const type = this.safeStringLower (order, 'child_order_type');
|
||||
const side = this.safeStringLower (order, 'side');
|
||||
let symbol = undefined;
|
||||
if (market === undefined) {
|
||||
const marketId = this.safeString (order, 'product_code');
|
||||
if (marketId in this.markets_by_id) {
|
||||
market = this.markets_by_id[marketId];
|
||||
}
|
||||
}
|
||||
if (market !== undefined) {
|
||||
symbol = market['symbol'];
|
||||
}
|
||||
let fee = undefined;
|
||||
const feeCost = this.safeFloat (order, 'total_commission');
|
||||
if (feeCost !== undefined) {
|
||||
fee = {
|
||||
'cost': feeCost,
|
||||
'currency': undefined,
|
||||
'rate': undefined,
|
||||
};
|
||||
}
|
||||
const id = this.safeString (order, 'child_order_acceptance_id');
|
||||
return {
|
||||
'id': id,
|
||||
'clientOrderId': undefined,
|
||||
'info': order,
|
||||
'timestamp': timestamp,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'lastTradeTimestamp': undefined,
|
||||
'status': status,
|
||||
'symbol': symbol,
|
||||
'type': type,
|
||||
'side': side,
|
||||
'price': price,
|
||||
'cost': cost,
|
||||
'amount': amount,
|
||||
'filled': filled,
|
||||
'remaining': remaining,
|
||||
'fee': fee,
|
||||
'average': undefined,
|
||||
'trades': undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async fetchOrders (symbol = undefined, since = undefined, limit = 100, params = {}) {
|
||||
if (symbol === undefined) {
|
||||
throw new ArgumentsRequired (this.id + ' fetchOrders() requires a `symbol` argument');
|
||||
}
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const request = {
|
||||
'product_code': market['id'],
|
||||
'count': limit,
|
||||
};
|
||||
const response = await this.privateGetGetchildorders (this.extend (request, params));
|
||||
let orders = this.parseOrders (response, market, since, limit);
|
||||
if (symbol !== undefined) {
|
||||
orders = this.filterBy (orders, 'symbol', symbol);
|
||||
}
|
||||
return orders;
|
||||
}
|
||||
|
||||
async fetchOpenOrders (symbol = undefined, since = undefined, limit = 100, params = {}) {
|
||||
const request = {
|
||||
'child_order_state': 'ACTIVE',
|
||||
};
|
||||
return await this.fetchOrders (symbol, since, limit, this.extend (request, params));
|
||||
}
|
||||
|
||||
async fetchClosedOrders (symbol = undefined, since = undefined, limit = 100, params = {}) {
|
||||
const request = {
|
||||
'child_order_state': 'COMPLETED',
|
||||
};
|
||||
return await this.fetchOrders (symbol, since, limit, this.extend (request, params));
|
||||
}
|
||||
|
||||
async fetchOrder (id, symbol = undefined, params = {}) {
|
||||
if (symbol === undefined) {
|
||||
throw new ArgumentsRequired (this.id + ' fetchOrder() requires a `symbol` argument');
|
||||
}
|
||||
const orders = await this.fetchOrders (symbol);
|
||||
const ordersById = this.indexBy (orders, 'id');
|
||||
if (id in ordersById) {
|
||||
return ordersById[id];
|
||||
}
|
||||
throw new OrderNotFound (this.id + ' No order found with id ' + id);
|
||||
}
|
||||
|
||||
async fetchMyTrades (symbol = undefined, since = undefined, limit = undefined, params = {}) {
|
||||
if (symbol === undefined) {
|
||||
throw new ArgumentsRequired (this.id + ' fetchMyTrades requires a `symbol` argument');
|
||||
}
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const request = {
|
||||
'product_code': market['id'],
|
||||
};
|
||||
if (limit !== undefined) {
|
||||
request['count'] = limit;
|
||||
}
|
||||
const response = await this.privateGetGetexecutions (this.extend (request, params));
|
||||
return this.parseTrades (response, market, since, limit);
|
||||
}
|
||||
|
||||
async withdraw (code, amount, address, tag = undefined, params = {}) {
|
||||
this.checkAddress (address);
|
||||
await this.loadMarkets ();
|
||||
if (code !== 'JPY' && code !== 'USD' && code !== 'EUR') {
|
||||
throw new ExchangeError (this.id + ' allows withdrawing JPY, USD, EUR only, ' + code + ' is not supported');
|
||||
}
|
||||
const currency = this.currency (code);
|
||||
const request = {
|
||||
'currency_code': currency['id'],
|
||||
'amount': amount,
|
||||
// 'bank_account_id': 1234,
|
||||
};
|
||||
const response = await this.privatePostWithdraw (this.extend (request, params));
|
||||
const id = this.safeString (response, 'message_id');
|
||||
return {
|
||||
'info': response,
|
||||
'id': id,
|
||||
};
|
||||
}
|
||||
|
||||
sign (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) {
|
||||
let request = '/' + this.version + '/';
|
||||
if (api === 'private') {
|
||||
request += 'me/';
|
||||
}
|
||||
request += path;
|
||||
if (method === 'GET') {
|
||||
if (Object.keys (params).length) {
|
||||
request += '?' + this.urlencode (params);
|
||||
}
|
||||
}
|
||||
const url = this.urls['api'] + request;
|
||||
if (api === 'private') {
|
||||
this.checkRequiredCredentials ();
|
||||
const nonce = this.nonce ().toString ();
|
||||
let auth = [ nonce, method, request ].join ('');
|
||||
if (Object.keys (params).length) {
|
||||
if (method !== 'GET') {
|
||||
body = this.json (params);
|
||||
auth += body;
|
||||
}
|
||||
}
|
||||
headers = {
|
||||
'ACCESS-KEY': this.apiKey,
|
||||
'ACCESS-TIMESTAMP': nonce,
|
||||
'ACCESS-SIGN': this.hmac (this.encode (auth), this.encode (this.secret)),
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
}
|
||||
return { 'url': url, 'method': method, 'body': body, 'headers': headers };
|
||||
}
|
||||
};
|
||||
+600
@@ -0,0 +1,600 @@
|
||||
'use strict';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const Exchange = require ('./base/Exchange');
|
||||
const { ExchangeError, AuthenticationError, OrderNotFound, InsufficientFunds, DDoSProtection } = require ('./base/errors');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
module.exports = class bitforex extends Exchange {
|
||||
describe () {
|
||||
return this.deepExtend (super.describe (), {
|
||||
'id': 'bitforex',
|
||||
'name': 'Bitforex',
|
||||
'countries': [ 'CN' ],
|
||||
'version': 'v1',
|
||||
'has': {
|
||||
'fetchBalance': true,
|
||||
'fetchMarkets': true,
|
||||
'createOrder': true,
|
||||
'cancelOrder': true,
|
||||
'fetchTicker': true,
|
||||
'fetchTickers': false,
|
||||
'fetchMyTrades': false,
|
||||
'fetchTrades': true,
|
||||
'fetchOrder': true,
|
||||
'fetchOrders': false,
|
||||
'fetchOpenOrders': true,
|
||||
'fetchClosedOrders': true,
|
||||
'fetchOHLCV': true,
|
||||
},
|
||||
'timeframes': {
|
||||
'1m': '1min',
|
||||
'5m': '5min',
|
||||
'15m': '15min',
|
||||
'30m': '30min',
|
||||
'1h': '1hour',
|
||||
'2h': '2hour',
|
||||
'4h': '4hour',
|
||||
'12h': '12hour',
|
||||
'1d': '1day',
|
||||
'1w': '1week',
|
||||
'1M': '1month',
|
||||
},
|
||||
'urls': {
|
||||
'logo': 'https://user-images.githubusercontent.com/1294454/44310033-69e9e600-a3d8-11e8-873d-54d74d1bc4e4.jpg',
|
||||
'api': 'https://api.bitforex.com',
|
||||
'www': 'https://www.bitforex.com',
|
||||
'doc': 'https://github.com/githubdev2020/API_Doc_en/wiki',
|
||||
'fees': 'https://help.bitforex.com/en_us/?cat=13',
|
||||
'referral': 'https://www.bitforex.com/en/invitationRegister?inviterId=1867438',
|
||||
},
|
||||
'api': {
|
||||
'public': {
|
||||
'get': [
|
||||
'api/v1/market/symbols',
|
||||
'api/v1/market/ticker',
|
||||
'api/v1/market/depth',
|
||||
'api/v1/market/trades',
|
||||
'api/v1/market/kline',
|
||||
],
|
||||
},
|
||||
'private': {
|
||||
'post': [
|
||||
'api/v1/fund/mainAccount',
|
||||
'api/v1/fund/allAccount',
|
||||
'api/v1/trade/placeOrder',
|
||||
'api/v1/trade/placeMultiOrder',
|
||||
'api/v1/trade/cancelOrder',
|
||||
'api/v1/trade/orderInfo',
|
||||
'api/v1/trade/orderInfos',
|
||||
],
|
||||
},
|
||||
},
|
||||
'fees': {
|
||||
'trading': {
|
||||
'tierBased': false,
|
||||
'percentage': true,
|
||||
'maker': 0.1 / 100,
|
||||
'taker': 0.1 / 100,
|
||||
},
|
||||
'funding': {
|
||||
'tierBased': false,
|
||||
'percentage': true,
|
||||
'deposit': {},
|
||||
'withdraw': {
|
||||
'BTC': 0.0005,
|
||||
'ETH': 0.01,
|
||||
'BCH': 0.0001,
|
||||
'LTC': 0.001,
|
||||
'ETC': 0.005,
|
||||
'USDT': 5,
|
||||
'CMCT': 30,
|
||||
'AION': 3,
|
||||
'LVT': 0,
|
||||
'DATA': 40,
|
||||
'RHP': 50,
|
||||
'NEO': 0,
|
||||
'AIDOC': 10,
|
||||
'BQT': 2,
|
||||
'R': 2,
|
||||
'DPY': 0.8,
|
||||
'GTC': 40,
|
||||
'AGI': 30,
|
||||
'DENT': 100,
|
||||
'SAN': 1,
|
||||
'SPANK': 8,
|
||||
'AID': 5,
|
||||
'OMG': 0.1,
|
||||
'BFT': 5,
|
||||
'SHOW': 150,
|
||||
'TRX': 20,
|
||||
'ABYSS': 10,
|
||||
'THM': 25,
|
||||
'ZIL': 20,
|
||||
'PPT': 0.2,
|
||||
'WTC': 0.4,
|
||||
'LRC': 7,
|
||||
'BNT': 1,
|
||||
'CTXC': 1,
|
||||
'MITH': 20,
|
||||
'TRUE': 4,
|
||||
'LYM': 10,
|
||||
'VEE': 100,
|
||||
'AUTO': 200,
|
||||
'REN': 50,
|
||||
'TIO': 2.5,
|
||||
'NGC': 1.5,
|
||||
'PST': 10,
|
||||
'CRE': 200,
|
||||
'IPC': 5,
|
||||
'PTT': 1000,
|
||||
'XMCT': 20,
|
||||
'ATMI': 40,
|
||||
'TERN': 40,
|
||||
'XLM': 0.01,
|
||||
'ODE': 15,
|
||||
'FTM': 100,
|
||||
'RTE': 100,
|
||||
'DCC': 100,
|
||||
'IMT': 500,
|
||||
'GOT': 3,
|
||||
'EGT': 500,
|
||||
'DACC': 1000,
|
||||
'UBEX': 500,
|
||||
'ABL': 100,
|
||||
'OLT': 100,
|
||||
'DAV': 40,
|
||||
'THRT': 10,
|
||||
'RMESH': 3,
|
||||
'UPP': 20,
|
||||
'SDT': 0,
|
||||
'SHR': 10,
|
||||
'MTV': 3,
|
||||
'ESS': 100,
|
||||
'MET': 3,
|
||||
'TTC': 20,
|
||||
'LXT': 10,
|
||||
'XCLP': 100,
|
||||
'LUK': 100,
|
||||
'UBC': 100,
|
||||
'DTX': 10,
|
||||
'BEAT': 20,
|
||||
'DEED': 2,
|
||||
'BGX': 3000,
|
||||
'PRL': 20,
|
||||
'ELY': 50,
|
||||
'CARD': 300,
|
||||
'SQR': 15,
|
||||
'VRA': 400,
|
||||
'BWX': 3500,
|
||||
'MAS': 75,
|
||||
'FLP': 0.6,
|
||||
'UNC': 300,
|
||||
'CRNC': 15,
|
||||
'MFG': 70,
|
||||
'ZXC': 70,
|
||||
'TRT': 30,
|
||||
'ZIX': 35,
|
||||
'XRA': 10,
|
||||
'AMO': 1600,
|
||||
'IPG': 3,
|
||||
'uDoo': 50,
|
||||
'URB': 30,
|
||||
'ARCONA': 3,
|
||||
'CRAD': 5,
|
||||
'NOBS': 1000,
|
||||
'ADF': 2,
|
||||
'ELF': 5,
|
||||
'LX': 20,
|
||||
'PATH': 15,
|
||||
'SILK': 120,
|
||||
'SKYFT': 50,
|
||||
'EDN': 50,
|
||||
'ADE': 50,
|
||||
'EDR': 10,
|
||||
'TIME': 0.25,
|
||||
'SPRK': 20,
|
||||
'QTUM': 0.01,
|
||||
'BF': 5,
|
||||
'ZPR': 100,
|
||||
'HYB': 10,
|
||||
'CAN': 30,
|
||||
'CEL': 10,
|
||||
'ATS': 50,
|
||||
'KCASH': 1,
|
||||
'ACT': 0.01,
|
||||
'MT': 300,
|
||||
'DXT': 30,
|
||||
'WAB': 4000,
|
||||
'HYDRO': 400,
|
||||
'LQD': 5,
|
||||
'OPTC': 200,
|
||||
'EQUAD': 80,
|
||||
'LATX': 50,
|
||||
'LEDU': 100,
|
||||
'RIT': 70,
|
||||
'ACDC': 500,
|
||||
'FSN': 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
'commonCurrencies': {
|
||||
'UOS': 'UOS Network',
|
||||
},
|
||||
'exceptions': {
|
||||
'4004': OrderNotFound,
|
||||
'1013': AuthenticationError,
|
||||
'1016': AuthenticationError,
|
||||
'3002': InsufficientFunds,
|
||||
'10204': DDoSProtection,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async fetchMarkets (params = {}) {
|
||||
const response = await this.publicGetApiV1MarketSymbols (params);
|
||||
const data = response['data'];
|
||||
const result = [];
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const market = data[i];
|
||||
const id = this.safeString (market, 'symbol');
|
||||
const symbolParts = id.split ('-');
|
||||
const baseId = symbolParts[2];
|
||||
const quoteId = symbolParts[1];
|
||||
const base = this.safeCurrencyCode (baseId);
|
||||
const quote = this.safeCurrencyCode (quoteId);
|
||||
const symbol = base + '/' + quote;
|
||||
const active = true;
|
||||
const precision = {
|
||||
'amount': this.safeInteger (market, 'amountPrecision'),
|
||||
'price': this.safeInteger (market, 'pricePrecision'),
|
||||
};
|
||||
const limits = {
|
||||
'amount': {
|
||||
'min': this.safeFloat (market, 'minOrderAmount'),
|
||||
'max': undefined,
|
||||
},
|
||||
'price': {
|
||||
'min': undefined,
|
||||
'max': undefined,
|
||||
},
|
||||
'cost': {
|
||||
'min': undefined,
|
||||
'max': undefined,
|
||||
},
|
||||
};
|
||||
result.push ({
|
||||
'id': id,
|
||||
'symbol': symbol,
|
||||
'base': base,
|
||||
'quote': quote,
|
||||
'baseId': baseId,
|
||||
'quoteId': quoteId,
|
||||
'active': active,
|
||||
'precision': precision,
|
||||
'limits': limits,
|
||||
'info': market,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
parseTrade (trade, market = undefined) {
|
||||
let symbol = undefined;
|
||||
if (market !== undefined) {
|
||||
symbol = market['symbol'];
|
||||
}
|
||||
const timestamp = this.safeInteger (trade, 'time');
|
||||
const id = this.safeString (trade, 'tid');
|
||||
const orderId = undefined;
|
||||
const amount = this.safeFloat (trade, 'amount');
|
||||
const price = this.safeFloat (trade, 'price');
|
||||
let cost = undefined;
|
||||
if (price !== undefined) {
|
||||
if (amount !== undefined) {
|
||||
cost = amount * price;
|
||||
}
|
||||
}
|
||||
const sideId = this.safeInteger (trade, 'direction');
|
||||
const side = this.parseSide (sideId);
|
||||
return {
|
||||
'info': trade,
|
||||
'id': id,
|
||||
'timestamp': timestamp,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'symbol': symbol,
|
||||
'type': undefined,
|
||||
'side': side,
|
||||
'price': price,
|
||||
'amount': amount,
|
||||
'cost': cost,
|
||||
'order': orderId,
|
||||
'fee': undefined,
|
||||
'takerOrMaker': undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async fetchTrades (symbol, since = undefined, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const request = {
|
||||
'symbol': this.marketId (symbol),
|
||||
};
|
||||
if (limit !== undefined) {
|
||||
request['size'] = limit;
|
||||
}
|
||||
const market = this.market (symbol);
|
||||
const response = await this.publicGetApiV1MarketTrades (this.extend (request, params));
|
||||
return this.parseTrades (response['data'], market, since, limit);
|
||||
}
|
||||
|
||||
async fetchBalance (params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const response = await this.privatePostApiV1FundAllAccount (params);
|
||||
const data = response['data'];
|
||||
const result = { 'info': response };
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const balance = data[i];
|
||||
const currencyId = this.safeString (balance, 'currency');
|
||||
const code = this.safeCurrencyCode (currencyId);
|
||||
const account = this.account ();
|
||||
account['used'] = this.safeFloat (balance, 'frozen');
|
||||
account['free'] = this.safeFloat (balance, 'active');
|
||||
account['total'] = this.safeFloat (balance, 'fix');
|
||||
result[code] = account;
|
||||
}
|
||||
return this.parseBalance (result);
|
||||
}
|
||||
|
||||
async fetchTicker (symbol, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.markets[symbol];
|
||||
const request = {
|
||||
'symbol': market['id'],
|
||||
};
|
||||
const response = await this.publicGetApiV1MarketTicker (this.extend (request, params));
|
||||
const data = response['data'];
|
||||
const timestamp = this.safeInteger (data, 'date');
|
||||
return {
|
||||
'symbol': symbol,
|
||||
'timestamp': timestamp,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'high': this.safeFloat (data, 'high'),
|
||||
'low': this.safeFloat (data, 'low'),
|
||||
'bid': this.safeFloat (data, 'buy'),
|
||||
'bidVolume': undefined,
|
||||
'ask': this.safeFloat (data, 'sell'),
|
||||
'askVolume': undefined,
|
||||
'vwap': undefined,
|
||||
'open': undefined,
|
||||
'close': this.safeFloat (data, 'last'),
|
||||
'last': this.safeFloat (data, 'last'),
|
||||
'previousClose': undefined,
|
||||
'change': undefined,
|
||||
'percentage': undefined,
|
||||
'average': undefined,
|
||||
'baseVolume': this.safeFloat (data, 'vol'),
|
||||
'quoteVolume': undefined,
|
||||
'info': response,
|
||||
};
|
||||
}
|
||||
|
||||
parseOHLCV (ohlcv, market = undefined, timeframe = '1m', since = undefined, limit = undefined) {
|
||||
return [
|
||||
this.safeInteger (ohlcv, 'time'),
|
||||
this.safeFloat (ohlcv, 'open'),
|
||||
this.safeFloat (ohlcv, 'high'),
|
||||
this.safeFloat (ohlcv, 'low'),
|
||||
this.safeFloat (ohlcv, 'close'),
|
||||
this.safeFloat (ohlcv, 'vol'),
|
||||
];
|
||||
}
|
||||
|
||||
async fetchOHLCV (symbol, timeframe = '1m', since = undefined, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const request = {
|
||||
'symbol': market['id'],
|
||||
'ktype': this.timeframes[timeframe],
|
||||
};
|
||||
if (limit !== undefined) {
|
||||
request['size'] = limit; // default 1, max 600
|
||||
}
|
||||
const response = await this.publicGetApiV1MarketKline (this.extend (request, params));
|
||||
const ohlcvs = this.safeValue (response, 'data', []);
|
||||
return this.parseOHLCVs (ohlcvs, market, timeframe, since, limit);
|
||||
}
|
||||
|
||||
async fetchOrderBook (symbol, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const marketId = this.marketId (symbol);
|
||||
const request = {
|
||||
'symbol': marketId,
|
||||
};
|
||||
if (limit !== undefined) {
|
||||
request['size'] = limit;
|
||||
}
|
||||
const response = await this.publicGetApiV1MarketDepth (this.extend (request, params));
|
||||
const data = this.safeValue (response, 'data');
|
||||
const timestamp = this.safeInteger (response, 'time');
|
||||
return this.parseOrderBook (data, timestamp, 'bids', 'asks', 'price', 'amount');
|
||||
}
|
||||
|
||||
parseOrderStatus (status) {
|
||||
const statuses = {
|
||||
'0': 'open',
|
||||
'1': 'open',
|
||||
'2': 'closed',
|
||||
'3': 'canceled',
|
||||
'4': 'canceled',
|
||||
};
|
||||
return (status in statuses) ? statuses[status] : status;
|
||||
}
|
||||
|
||||
parseSide (sideId) {
|
||||
if (sideId === 1) {
|
||||
return 'buy';
|
||||
} else if (sideId === 2) {
|
||||
return 'sell';
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
parseOrder (order, market = undefined) {
|
||||
const id = this.safeString (order, 'orderId');
|
||||
const timestamp = this.safeFloat (order, 'createTime');
|
||||
const lastTradeTimestamp = this.safeFloat (order, 'lastTime');
|
||||
const symbol = market['symbol'];
|
||||
const sideId = this.safeInteger (order, 'tradeType');
|
||||
const side = this.parseSide (sideId);
|
||||
const type = undefined;
|
||||
const price = this.safeFloat (order, 'orderPrice');
|
||||
const average = this.safeFloat (order, 'avgPrice');
|
||||
const amount = this.safeFloat (order, 'orderAmount');
|
||||
const filled = this.safeFloat (order, 'dealAmount');
|
||||
const remaining = amount - filled;
|
||||
const status = this.parseOrderStatus (this.safeString (order, 'orderState'));
|
||||
const cost = filled * price;
|
||||
const feeSide = (side === 'buy') ? 'base' : 'quote';
|
||||
const feeCurrency = market[feeSide];
|
||||
const fee = {
|
||||
'cost': this.safeFloat (order, 'tradeFee'),
|
||||
'currency': feeCurrency,
|
||||
};
|
||||
const result = {
|
||||
'info': order,
|
||||
'id': id,
|
||||
'clientOrderId': undefined,
|
||||
'timestamp': timestamp,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'lastTradeTimestamp': lastTradeTimestamp,
|
||||
'symbol': symbol,
|
||||
'type': type,
|
||||
'side': side,
|
||||
'price': price,
|
||||
'cost': cost,
|
||||
'average': average,
|
||||
'amount': amount,
|
||||
'filled': filled,
|
||||
'remaining': remaining,
|
||||
'status': status,
|
||||
'fee': fee,
|
||||
'trades': undefined,
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
async fetchOrder (id, symbol = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const request = {
|
||||
'symbol': this.marketId (symbol),
|
||||
'orderId': id,
|
||||
};
|
||||
const response = await this.privatePostApiV1TradeOrderInfo (this.extend (request, params));
|
||||
const order = this.parseOrder (response['data'], market);
|
||||
return order;
|
||||
}
|
||||
|
||||
async fetchOpenOrders (symbol = undefined, since = undefined, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const request = {
|
||||
'symbol': this.marketId (symbol),
|
||||
'state': 0,
|
||||
};
|
||||
const response = await this.privatePostApiV1TradeOrderInfos (this.extend (request, params));
|
||||
return this.parseOrders (response['data'], market, since, limit);
|
||||
}
|
||||
|
||||
async fetchClosedOrders (symbol = undefined, since = undefined, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const request = {
|
||||
'symbol': this.marketId (symbol),
|
||||
'state': 1,
|
||||
};
|
||||
const response = await this.privatePostApiV1TradeOrderInfos (this.extend (request, params));
|
||||
return this.parseOrders (response['data'], market, since, limit);
|
||||
}
|
||||
|
||||
async createOrder (symbol, type, side, amount, price = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
let sideId = undefined;
|
||||
if (side === 'buy') {
|
||||
sideId = 1;
|
||||
} else if (side === 'sell') {
|
||||
sideId = 2;
|
||||
}
|
||||
const request = {
|
||||
'symbol': this.marketId (symbol),
|
||||
'price': price,
|
||||
'amount': amount,
|
||||
'tradeType': sideId,
|
||||
};
|
||||
const response = await this.privatePostApiV1TradePlaceOrder (this.extend (request, params));
|
||||
const data = response['data'];
|
||||
return {
|
||||
'info': response,
|
||||
'id': this.safeString (data, 'orderId'),
|
||||
};
|
||||
}
|
||||
|
||||
async cancelOrder (id, symbol = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const request = {
|
||||
'orderId': id,
|
||||
};
|
||||
if (symbol !== undefined) {
|
||||
request['symbol'] = this.marketId (symbol);
|
||||
}
|
||||
const results = await this.privatePostApiV1TradeCancelOrder (this.extend (request, params));
|
||||
const success = results['success'];
|
||||
const returnVal = { 'info': results, 'success': success };
|
||||
return returnVal;
|
||||
}
|
||||
|
||||
sign (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) {
|
||||
let url = this.urls['api'] + '/' + this.implodeParams (path, params);
|
||||
const query = this.omit (params, this.extractParams (path));
|
||||
if (api === 'public') {
|
||||
if (Object.keys (query).length) {
|
||||
url += '?' + this.urlencode (query);
|
||||
}
|
||||
} else {
|
||||
this.checkRequiredCredentials ();
|
||||
let payload = this.urlencode ({ 'accessKey': this.apiKey });
|
||||
query['nonce'] = this.milliseconds ();
|
||||
if (Object.keys (query).length) {
|
||||
payload += '&' + this.urlencode (this.keysort (query));
|
||||
}
|
||||
// let message = '/' + 'api/' + this.version + '/' + path + '?' + payload;
|
||||
const message = '/' + path + '?' + payload;
|
||||
const signature = this.hmac (this.encode (message), this.encode (this.secret));
|
||||
body = payload + '&signData=' + signature;
|
||||
headers = {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
};
|
||||
}
|
||||
return { 'url': url, 'method': method, 'body': body, 'headers': headers };
|
||||
}
|
||||
|
||||
handleErrors (code, reason, url, method, headers, body, response, requestHeaders, requestBody) {
|
||||
if (typeof body !== 'string') {
|
||||
return; // fallback to default error handler
|
||||
}
|
||||
if ((body[0] === '{') || (body[0] === '[')) {
|
||||
const feedback = this.id + ' ' + body;
|
||||
const success = this.safeValue (response, 'success');
|
||||
if (success !== undefined) {
|
||||
if (!success) {
|
||||
const code = this.safeString (response, 'code');
|
||||
this.throwExactlyMatchedException (this.exceptions, code, feedback);
|
||||
throw new ExchangeError (feedback);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
+800
@@ -0,0 +1,800 @@
|
||||
'use strict';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const Exchange = require ('./base/Exchange');
|
||||
const { ExchangeError, ExchangeNotAvailable, AuthenticationError, BadRequest, PermissionDenied, InvalidAddress, ArgumentsRequired, InvalidOrder } = require ('./base/errors');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
module.exports = class bithumb extends Exchange {
|
||||
describe () {
|
||||
return this.deepExtend (super.describe (), {
|
||||
'id': 'bithumb',
|
||||
'name': 'Bithumb',
|
||||
'countries': [ 'KR' ], // South Korea
|
||||
'rateLimit': 500,
|
||||
'has': {
|
||||
'CORS': true,
|
||||
'createOrder': true,
|
||||
'cancelOrder': true,
|
||||
'createMarketOrder': true,
|
||||
'fetchTickers': true,
|
||||
'fetchOpenOrders': true,
|
||||
'fetchOrder': true,
|
||||
'withdraw': true,
|
||||
},
|
||||
'urls': {
|
||||
'logo': 'https://user-images.githubusercontent.com/1294454/30597177-ea800172-9d5e-11e7-804c-b9d4fa9b56b0.jpg',
|
||||
'api': {
|
||||
'public': 'https://api.bithumb.com/public',
|
||||
'private': 'https://api.bithumb.com',
|
||||
},
|
||||
'www': 'https://www.bithumb.com',
|
||||
'doc': 'https://apidocs.bithumb.com',
|
||||
'fees': 'https://en.bithumb.com/customer_support/info_fee',
|
||||
},
|
||||
'api': {
|
||||
'public': {
|
||||
'get': [
|
||||
'ticker/{currency}',
|
||||
'ticker/all',
|
||||
'orderbook/{currency}',
|
||||
'orderbook/all',
|
||||
'transaction_history/{currency}',
|
||||
'transaction_history/all',
|
||||
],
|
||||
},
|
||||
'private': {
|
||||
'post': [
|
||||
'info/account',
|
||||
'info/balance',
|
||||
'info/wallet_address',
|
||||
'info/ticker',
|
||||
'info/orders',
|
||||
'info/user_transactions',
|
||||
'info/order_detail',
|
||||
'trade/place',
|
||||
'trade/cancel',
|
||||
'trade/btc_withdrawal',
|
||||
'trade/krw_deposit',
|
||||
'trade/krw_withdrawal',
|
||||
'trade/market_buy',
|
||||
'trade/market_sell',
|
||||
],
|
||||
},
|
||||
},
|
||||
'fees': {
|
||||
'trading': {
|
||||
'maker': 0.25 / 100,
|
||||
'taker': 0.25 / 100,
|
||||
},
|
||||
},
|
||||
'exceptions': {
|
||||
'Bad Request(SSL)': BadRequest,
|
||||
'Bad Request(Bad Method)': BadRequest,
|
||||
'Bad Request.(Auth Data)': AuthenticationError, // { "status": "5100", "message": "Bad Request.(Auth Data)" }
|
||||
'Not Member': AuthenticationError,
|
||||
'Invalid Apikey': AuthenticationError, // {"status":"5300","message":"Invalid Apikey"}
|
||||
'Method Not Allowed.(Access IP)': PermissionDenied,
|
||||
'Method Not Allowed.(BTC Adress)': InvalidAddress,
|
||||
'Method Not Allowed.(Access)': PermissionDenied,
|
||||
'Database Fail': ExchangeNotAvailable,
|
||||
'Invalid Parameter': BadRequest,
|
||||
'5600': ExchangeError,
|
||||
'Unknown Error': ExchangeError,
|
||||
'After May 23th, recent_transactions is no longer, hence users will not be able to connect to recent_transactions': ExchangeError, // {"status":"5100","message":"After May 23th, recent_transactions is no longer, hence users will not be able to connect to recent_transactions"}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async fetchMarkets (params = {}) {
|
||||
const response = await this.publicGetTickerAll (params);
|
||||
const data = this.safeValue (response, 'data');
|
||||
const currencyIds = Object.keys (data);
|
||||
const result = [];
|
||||
for (let i = 0; i < currencyIds.length; i++) {
|
||||
const currencyId = currencyIds[i];
|
||||
if (currencyId === 'date') {
|
||||
continue;
|
||||
}
|
||||
const market = data[currencyId];
|
||||
const base = currencyId;
|
||||
const quote = 'KRW';
|
||||
const symbol = currencyId + '/' + quote;
|
||||
let active = true;
|
||||
if (Array.isArray (market)) {
|
||||
const numElements = market.length;
|
||||
if (numElements === 0) {
|
||||
active = false;
|
||||
}
|
||||
}
|
||||
result.push ({
|
||||
'id': currencyId,
|
||||
'symbol': symbol,
|
||||
'base': base,
|
||||
'quote': quote,
|
||||
'info': market,
|
||||
'active': active,
|
||||
'precision': {
|
||||
'amount': undefined,
|
||||
'price': undefined,
|
||||
},
|
||||
'limits': {
|
||||
'amount': {
|
||||
'min': undefined,
|
||||
'max': undefined,
|
||||
},
|
||||
'price': {
|
||||
'min': undefined,
|
||||
'max': undefined,
|
||||
},
|
||||
'cost': {
|
||||
'min': undefined,
|
||||
'max': undefined,
|
||||
},
|
||||
},
|
||||
'baseId': undefined,
|
||||
'quoteId': undefined,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async fetchBalance (params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const request = {
|
||||
'currency': 'ALL',
|
||||
};
|
||||
const response = await this.privatePostInfoBalance (this.extend (request, params));
|
||||
const result = { 'info': response };
|
||||
const balances = this.safeValue (response, 'data');
|
||||
const codes = Object.keys (this.currencies);
|
||||
for (let i = 0; i < codes.length; i++) {
|
||||
const code = codes[i];
|
||||
const account = this.account ();
|
||||
const currency = this.currency (code);
|
||||
const lowerCurrencyId = this.safeStringLower (currency, 'id');
|
||||
account['total'] = this.safeFloat (balances, 'total_' + lowerCurrencyId);
|
||||
account['used'] = this.safeFloat (balances, 'in_use_' + lowerCurrencyId);
|
||||
account['free'] = this.safeFloat (balances, 'available_' + lowerCurrencyId);
|
||||
result[code] = account;
|
||||
}
|
||||
return this.parseBalance (result);
|
||||
}
|
||||
|
||||
async fetchOrderBook (symbol, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const request = {
|
||||
'currency': market['base'],
|
||||
};
|
||||
if (limit !== undefined) {
|
||||
request['count'] = limit; // default 30, max 30
|
||||
}
|
||||
const response = await this.publicGetOrderbookCurrency (this.extend (request, params));
|
||||
//
|
||||
// {
|
||||
// "status":"0000",
|
||||
// "data":{
|
||||
// "timestamp":"1587621553942",
|
||||
// "payment_currency":"KRW",
|
||||
// "order_currency":"BTC",
|
||||
// "bids":[
|
||||
// {"price":"8652000","quantity":"0.0043"},
|
||||
// {"price":"8651000","quantity":"0.0049"},
|
||||
// {"price":"8650000","quantity":"8.4791"},
|
||||
// ],
|
||||
// "asks":[
|
||||
// {"price":"8654000","quantity":"0.119"},
|
||||
// {"price":"8655000","quantity":"0.254"},
|
||||
// {"price":"8658000","quantity":"0.119"},
|
||||
// ]
|
||||
// }
|
||||
// }
|
||||
//
|
||||
const data = this.safeValue (response, 'data', {});
|
||||
const timestamp = this.safeInteger (data, 'timestamp');
|
||||
return this.parseOrderBook (data, timestamp, 'bids', 'asks', 'price', 'quantity');
|
||||
}
|
||||
|
||||
parseTicker (ticker, market = undefined) {
|
||||
//
|
||||
// fetchTicker, fetchTickers
|
||||
//
|
||||
// {
|
||||
// "opening_price":"227100",
|
||||
// "closing_price":"228400",
|
||||
// "min_price":"222300",
|
||||
// "max_price":"230000",
|
||||
// "units_traded":"82618.56075337",
|
||||
// "acc_trade_value":"18767376138.6031",
|
||||
// "prev_closing_price":"227100",
|
||||
// "units_traded_24H":"151871.13484676",
|
||||
// "acc_trade_value_24H":"34247610416.8974",
|
||||
// "fluctate_24H":"8700",
|
||||
// "fluctate_rate_24H":"3.96",
|
||||
// "date":"1587710327264", // fetchTickers inject this
|
||||
// }
|
||||
//
|
||||
const timestamp = this.safeInteger (ticker, 'date');
|
||||
let symbol = undefined;
|
||||
if (market !== undefined) {
|
||||
symbol = market['symbol'];
|
||||
}
|
||||
const open = this.safeFloat (ticker, 'opening_price');
|
||||
const close = this.safeFloat (ticker, 'closing_price');
|
||||
let change = undefined;
|
||||
let percentage = undefined;
|
||||
let average = undefined;
|
||||
if ((close !== undefined) && (open !== undefined)) {
|
||||
change = close - open;
|
||||
if (open > 0) {
|
||||
percentage = change / open * 100;
|
||||
}
|
||||
average = this.sum (open, close) / 2;
|
||||
}
|
||||
const baseVolume = this.safeFloat (ticker, 'units_traded_24H');
|
||||
const quoteVolume = this.safeFloat (ticker, 'acc_trade_value_24H');
|
||||
let vwap = undefined;
|
||||
if (quoteVolume !== undefined && baseVolume !== undefined) {
|
||||
vwap = quoteVolume / baseVolume;
|
||||
}
|
||||
return {
|
||||
'symbol': symbol,
|
||||
'timestamp': timestamp,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'high': this.safeFloat (ticker, 'max_price'),
|
||||
'low': this.safeFloat (ticker, 'min_price'),
|
||||
'bid': this.safeFloat (ticker, 'buy_price'),
|
||||
'bidVolume': undefined,
|
||||
'ask': this.safeFloat (ticker, 'sell_price'),
|
||||
'askVolume': undefined,
|
||||
'vwap': vwap,
|
||||
'open': open,
|
||||
'close': close,
|
||||
'last': close,
|
||||
'previousClose': undefined,
|
||||
'change': change,
|
||||
'percentage': percentage,
|
||||
'average': average,
|
||||
'baseVolume': baseVolume,
|
||||
'quoteVolume': quoteVolume,
|
||||
'info': ticker,
|
||||
};
|
||||
}
|
||||
|
||||
async fetchTickers (symbols = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const response = await this.publicGetTickerAll (params);
|
||||
//
|
||||
// {
|
||||
// "status":"0000",
|
||||
// "data":{
|
||||
// "BTC":{
|
||||
// "opening_price":"9045000",
|
||||
// "closing_price":"9132000",
|
||||
// "min_price":"8938000",
|
||||
// "max_price":"9168000",
|
||||
// "units_traded":"4619.79967497",
|
||||
// "acc_trade_value":"42021363832.5187",
|
||||
// "prev_closing_price":"9041000",
|
||||
// "units_traded_24H":"8793.5045804",
|
||||
// "acc_trade_value_24H":"78933458515.4962",
|
||||
// "fluctate_24H":"530000",
|
||||
// "fluctate_rate_24H":"6.16"
|
||||
// },
|
||||
// "date":"1587710878669"
|
||||
// }
|
||||
// }
|
||||
//
|
||||
const result = {};
|
||||
const data = this.safeValue (response, 'data', {});
|
||||
const timestamp = this.safeInteger (data, 'date');
|
||||
const tickers = this.omit (data, 'date');
|
||||
const ids = Object.keys (tickers);
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
const id = ids[i];
|
||||
let symbol = id;
|
||||
let market = undefined;
|
||||
if (id in this.markets_by_id) {
|
||||
market = this.markets_by_id[id];
|
||||
symbol = market['symbol'];
|
||||
}
|
||||
const ticker = tickers[id];
|
||||
const isArray = Array.isArray (ticker);
|
||||
if (!isArray) {
|
||||
ticker['date'] = timestamp;
|
||||
result[symbol] = this.parseTicker (ticker, market);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async fetchTicker (symbol, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const request = {
|
||||
'currency': market['base'],
|
||||
};
|
||||
const response = await this.publicGetTickerCurrency (this.extend (request, params));
|
||||
//
|
||||
// {
|
||||
// "status":"0000",
|
||||
// "data":{
|
||||
// "opening_price":"227100",
|
||||
// "closing_price":"228400",
|
||||
// "min_price":"222300",
|
||||
// "max_price":"230000",
|
||||
// "units_traded":"82618.56075337",
|
||||
// "acc_trade_value":"18767376138.6031",
|
||||
// "prev_closing_price":"227100",
|
||||
// "units_traded_24H":"151871.13484676",
|
||||
// "acc_trade_value_24H":"34247610416.8974",
|
||||
// "fluctate_24H":"8700",
|
||||
// "fluctate_rate_24H":"3.96",
|
||||
// "date":"1587710327264"
|
||||
// }
|
||||
// }
|
||||
//
|
||||
const data = this.safeValue (response, 'data', {});
|
||||
return this.parseTicker (data, market);
|
||||
}
|
||||
|
||||
parseTrade (trade, market = undefined) {
|
||||
//
|
||||
// fetchTrades (public)
|
||||
//
|
||||
// {
|
||||
// "transaction_date":"2020-04-23 22:21:46",
|
||||
// "type":"ask",
|
||||
// "units_traded":"0.0125",
|
||||
// "price":"8667000",
|
||||
// "total":"108337"
|
||||
// }
|
||||
//
|
||||
// fetchOrder (private)
|
||||
//
|
||||
// {
|
||||
// "transaction_date": "1572497603902030",
|
||||
// "price": "8601000",
|
||||
// "units": "0.005",
|
||||
// "fee_currency": "KRW",
|
||||
// "fee": "107.51",
|
||||
// "total": "43005"
|
||||
// }
|
||||
//
|
||||
// a workaround for their bug in date format, hours are not 0-padded
|
||||
let timestamp = undefined;
|
||||
const transactionDatetime = this.safeString (trade, 'transaction_date');
|
||||
if (transactionDatetime !== undefined) {
|
||||
const parts = transactionDatetime.split (' ');
|
||||
const numParts = parts.length;
|
||||
if (numParts > 1) {
|
||||
const transactionDate = parts[0];
|
||||
let transactionTime = parts[1];
|
||||
if (transactionTime.length < 8) {
|
||||
transactionTime = '0' + transactionTime;
|
||||
}
|
||||
timestamp = this.parse8601 (transactionDate + ' ' + transactionTime);
|
||||
} else {
|
||||
timestamp = this.safeIntegerProduct (trade, 'transaction_date', 0.001);
|
||||
}
|
||||
}
|
||||
if (timestamp !== undefined) {
|
||||
timestamp -= 9 * 3600000; // they report UTC + 9 hours, server in Korean timezone
|
||||
}
|
||||
const type = undefined;
|
||||
let side = this.safeString (trade, 'type');
|
||||
side = (side === 'ask') ? 'sell' : 'buy';
|
||||
const id = this.safeString (trade, 'cont_no');
|
||||
let symbol = undefined;
|
||||
if (market !== undefined) {
|
||||
symbol = market['symbol'];
|
||||
}
|
||||
const price = this.safeFloat (trade, 'price');
|
||||
const amount = this.safeFloat (trade, 'units_traded');
|
||||
let cost = this.safeFloat (trade, 'total');
|
||||
if (cost === undefined) {
|
||||
if (amount !== undefined) {
|
||||
if (price !== undefined) {
|
||||
cost = price * amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
let fee = undefined;
|
||||
const feeCost = this.safeFloat (trade, 'fee');
|
||||
if (feeCost !== undefined) {
|
||||
const feeCurrencyId = this.safeString (trade, 'fee_currency');
|
||||
const feeCurrencyCode = this.commonCurrencyCode (feeCurrencyId);
|
||||
fee = {
|
||||
'cost': feeCost,
|
||||
'currency': feeCurrencyCode,
|
||||
};
|
||||
}
|
||||
return {
|
||||
'id': id,
|
||||
'info': trade,
|
||||
'timestamp': timestamp,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'symbol': symbol,
|
||||
'order': undefined,
|
||||
'type': type,
|
||||
'side': side,
|
||||
'takerOrMaker': undefined,
|
||||
'price': price,
|
||||
'amount': amount,
|
||||
'cost': cost,
|
||||
'fee': fee,
|
||||
};
|
||||
}
|
||||
|
||||
async fetchTrades (symbol, since = undefined, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const request = {
|
||||
'currency': market['base'],
|
||||
};
|
||||
if (limit === undefined) {
|
||||
request['count'] = limit; // default 20, max 100
|
||||
}
|
||||
const response = await this.publicGetTransactionHistoryCurrency (this.extend (request, params));
|
||||
//
|
||||
// {
|
||||
// "status":"0000",
|
||||
// "data":[
|
||||
// {
|
||||
// "transaction_date":"2020-04-23 22:21:46",
|
||||
// "type":"ask",
|
||||
// "units_traded":"0.0125",
|
||||
// "price":"8667000",
|
||||
// "total":"108337"
|
||||
// },
|
||||
// ]
|
||||
// }
|
||||
//
|
||||
const data = this.safeValue (response, 'data', []);
|
||||
return this.parseTrades (data, market, since, limit);
|
||||
}
|
||||
|
||||
async createOrder (symbol, type, side, amount, price = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const request = {
|
||||
'order_currency': market['id'],
|
||||
'Payment_currency': market['quote'],
|
||||
'units': amount,
|
||||
};
|
||||
let method = 'privatePostTradePlace';
|
||||
if (type === 'limit') {
|
||||
request['price'] = price;
|
||||
request['type'] = (side === 'buy') ? 'bid' : 'ask';
|
||||
} else {
|
||||
method = 'privatePostTradeMarket' + this.capitalize (side);
|
||||
}
|
||||
const response = await this[method] (this.extend (request, params));
|
||||
const id = this.safeString (response, 'order_id');
|
||||
if (id === undefined) {
|
||||
throw new InvalidOrder (this.id + ' createOrder did not return an order id');
|
||||
}
|
||||
return {
|
||||
'info': response,
|
||||
'symbol': symbol,
|
||||
'type': type,
|
||||
'side': side,
|
||||
'id': id,
|
||||
};
|
||||
}
|
||||
|
||||
async fetchOrder (id, symbol = undefined, params = {}) {
|
||||
if (symbol === undefined) {
|
||||
throw new ArgumentsRequired (this.id + ' fetchOrder requires a symbol argument');
|
||||
}
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const request = {
|
||||
'order_id': id,
|
||||
'count': 1,
|
||||
'order_currency': market['base'],
|
||||
'payment_currency': market['quote'],
|
||||
};
|
||||
const response = await this.privatePostInfoOrderDetail (this.extend (request, params));
|
||||
//
|
||||
// {
|
||||
// "status": "0000",
|
||||
// "data": {
|
||||
// "transaction_date": "1572497603668315",
|
||||
// "type": "bid",
|
||||
// "order_status": "Completed",
|
||||
// "order_currency": "BTC",
|
||||
// "payment_currency": "KRW",
|
||||
// "order_price": "8601000",
|
||||
// "order_qty": "0.007",
|
||||
// "cancel_date": "",
|
||||
// "cancel_type": "",
|
||||
// "contract": [
|
||||
// {
|
||||
// "transaction_date": "1572497603902030",
|
||||
// "price": "8601000",
|
||||
// "units": "0.005",
|
||||
// "fee_currency": "KRW",
|
||||
// "fee": "107.51",
|
||||
// "total": "43005"
|
||||
// },
|
||||
// ]
|
||||
// }
|
||||
// }
|
||||
//
|
||||
const data = this.safeValue (response, 'data');
|
||||
return this.parseOrder (this.extend (data, { 'order_id': id }), market);
|
||||
}
|
||||
|
||||
parseOrderStatus (status) {
|
||||
const statuses = {
|
||||
'Pending': 'open',
|
||||
'Completed': 'closed',
|
||||
'Cancel': 'canceled',
|
||||
};
|
||||
return this.safeString (statuses, status, status);
|
||||
}
|
||||
|
||||
parseOrder (order, market = undefined) {
|
||||
//
|
||||
// fetchOrder
|
||||
//
|
||||
// {
|
||||
// "transaction_date": "1572497603668315",
|
||||
// "type": "bid",
|
||||
// "order_status": "Completed",
|
||||
// "order_currency": "BTC",
|
||||
// "payment_currency": "KRW",
|
||||
// "order_price": "8601000",
|
||||
// "order_qty": "0.007",
|
||||
// "cancel_date": "",
|
||||
// "cancel_type": "",
|
||||
// "contract": [
|
||||
// {
|
||||
// "transaction_date": "1572497603902030",
|
||||
// "price": "8601000",
|
||||
// "units": "0.005",
|
||||
// "fee_currency": "KRW",
|
||||
// "fee": "107.51",
|
||||
// "total": "43005"
|
||||
// },
|
||||
// ]
|
||||
// }
|
||||
//
|
||||
// fetchOpenOrders
|
||||
//
|
||||
// {
|
||||
// "order_currency": "BTC",
|
||||
// "payment_currency": "KRW",
|
||||
// "order_id": "C0101000007408440032",
|
||||
// "order_date": "1571728739360570",
|
||||
// "type": "bid",
|
||||
// "units": "5.0",
|
||||
// "units_remaining": "5.0",
|
||||
// "price": "501000",
|
||||
// }
|
||||
//
|
||||
const timestamp = this.safeIntegerProduct (order, 'order_date', 0.001);
|
||||
const sideProperty = this.safeValue2 (order, 'type', 'side');
|
||||
const side = (sideProperty === 'bid') ? 'buy' : 'sell';
|
||||
const status = this.parseOrderStatus (this.safeString (order, 'order_status'));
|
||||
let price = this.safeFloat2 (order, 'order_price', 'price');
|
||||
let type = 'limit';
|
||||
if (price === 0) {
|
||||
price = undefined;
|
||||
type = 'market';
|
||||
}
|
||||
const amount = this.safeFloat2 (order, 'order_qty', 'units');
|
||||
let remaining = this.safeFloat (order, 'units_remaining');
|
||||
if (remaining === undefined) {
|
||||
if (status === 'closed') {
|
||||
remaining = 0;
|
||||
} else {
|
||||
remaining = amount;
|
||||
}
|
||||
}
|
||||
let filled = undefined;
|
||||
if ((amount !== undefined) && (remaining !== undefined)) {
|
||||
filled = amount - remaining;
|
||||
}
|
||||
let symbol = undefined;
|
||||
const baseId = this.safeString (order, 'order_currency');
|
||||
const quoteId = this.safeString (order, 'payment_currency');
|
||||
const base = this.safeCurrencyCode (baseId);
|
||||
const quote = this.safeCurrencyCode (quoteId);
|
||||
if ((base !== undefined) && (quote !== undefined)) {
|
||||
symbol = base + '/' + quote;
|
||||
}
|
||||
if ((symbol === undefined) && (market !== undefined)) {
|
||||
symbol = market['symbol'];
|
||||
}
|
||||
const rawTrades = this.safeValue (order, 'contract');
|
||||
let trades = undefined;
|
||||
const id = this.safeString (order, 'order_id');
|
||||
if (rawTrades !== undefined) {
|
||||
trades = this.parseTrades (rawTrades, market, undefined, undefined, {
|
||||
'side': side,
|
||||
'symbol': symbol,
|
||||
'order': id,
|
||||
});
|
||||
}
|
||||
return {
|
||||
'info': order,
|
||||
'id': id,
|
||||
'timestamp': timestamp,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'lastTradeTimestamp': undefined,
|
||||
'symbol': symbol,
|
||||
'type': type,
|
||||
'side': side,
|
||||
'price': price,
|
||||
'amount': amount,
|
||||
'cost': undefined,
|
||||
'average': undefined,
|
||||
'filled': filled,
|
||||
'remaining': remaining,
|
||||
'status': status,
|
||||
'fee': undefined,
|
||||
'trades': trades,
|
||||
};
|
||||
}
|
||||
|
||||
async fetchOpenOrders (symbol = undefined, since = undefined, limit = undefined, params = {}) {
|
||||
if (symbol === undefined) {
|
||||
throw new ArgumentsRequired (this.id + ' fetchOpenOrders requires a symbol argument');
|
||||
}
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
if (limit === undefined) {
|
||||
limit = 100;
|
||||
}
|
||||
const request = {
|
||||
'count': limit,
|
||||
'order_currency': market['base'],
|
||||
'payment_currency': market['quote'],
|
||||
};
|
||||
if (since !== undefined) {
|
||||
request['after'] = since;
|
||||
}
|
||||
const response = await this.privatePostInfoOrders (this.extend (request, params));
|
||||
//
|
||||
// {
|
||||
// "status": "0000",
|
||||
// "data": [
|
||||
// {
|
||||
// "order_currency": "BTC",
|
||||
// "payment_currency": "KRW",
|
||||
// "order_id": "C0101000007408440032",
|
||||
// "order_date": "1571728739360570",
|
||||
// "type": "bid",
|
||||
// "units": "5.0",
|
||||
// "units_remaining": "5.0",
|
||||
// "price": "501000",
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
//
|
||||
const data = this.safeValue (response, 'data', []);
|
||||
return this.parseOrders (data, market, since, limit);
|
||||
}
|
||||
|
||||
async cancelOrder (id, symbol = undefined, params = {}) {
|
||||
const side_in_params = ('side' in params);
|
||||
if (!side_in_params) {
|
||||
throw new ArgumentsRequired (this.id + ' cancelOrder requires a `symbol` argument and a `side` parameter (sell or buy)');
|
||||
}
|
||||
if (symbol === undefined) {
|
||||
throw new ArgumentsRequired (this.id + ' cancelOrder requires a `symbol` argument and a `side` parameter (sell or buy)');
|
||||
}
|
||||
const market = this.market (symbol);
|
||||
const side = (params['side'] === 'buy') ? 'bid' : 'ask';
|
||||
params = this.omit (params, [ 'side', 'currency' ]);
|
||||
// https://github.com/ccxt/ccxt/issues/6771
|
||||
const request = {
|
||||
'order_id': id,
|
||||
'type': side,
|
||||
'order_currency': market['base'],
|
||||
'payment_currency': market['quote'],
|
||||
};
|
||||
return await this.privatePostTradeCancel (this.extend (request, params));
|
||||
}
|
||||
|
||||
cancelUnifiedOrder (order, params = {}) {
|
||||
const request = {
|
||||
'side': order['side'],
|
||||
};
|
||||
return this.cancelOrder (order['id'], order['symbol'], this.extend (request, params));
|
||||
}
|
||||
|
||||
async withdraw (code, amount, address, tag = undefined, params = {}) {
|
||||
this.checkAddress (address);
|
||||
await this.loadMarkets ();
|
||||
const currency = this.currency (code);
|
||||
const request = {
|
||||
'units': amount,
|
||||
'address': address,
|
||||
'currency': currency['id'],
|
||||
};
|
||||
if (currency === 'XRP' || currency === 'XMR') {
|
||||
const destination = this.safeString (params, 'destination');
|
||||
if ((tag === undefined) && (destination === undefined)) {
|
||||
throw new ArgumentsRequired (this.id + ' ' + code + ' withdraw() requires a tag argument or an extra destination param');
|
||||
} else if (tag !== undefined) {
|
||||
request['destination'] = tag;
|
||||
}
|
||||
}
|
||||
const response = await this.privatePostTradeBtcWithdrawal (this.extend (request, params));
|
||||
return {
|
||||
'info': response,
|
||||
'id': undefined,
|
||||
};
|
||||
}
|
||||
|
||||
nonce () {
|
||||
return this.milliseconds ();
|
||||
}
|
||||
|
||||
sign (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) {
|
||||
const endpoint = '/' + this.implodeParams (path, params);
|
||||
let url = this.urls['api'][api] + endpoint;
|
||||
const query = this.omit (params, this.extractParams (path));
|
||||
if (api === 'public') {
|
||||
if (Object.keys (query).length) {
|
||||
url += '?' + this.urlencode (query);
|
||||
}
|
||||
} else {
|
||||
this.checkRequiredCredentials ();
|
||||
body = this.urlencode (this.extend ({
|
||||
'endpoint': endpoint,
|
||||
}, query));
|
||||
const nonce = this.nonce ().toString ();
|
||||
const auth = endpoint + "\0" + body + "\0" + nonce; // eslint-disable-line quotes
|
||||
const signature = this.hmac (this.encode (auth), this.encode (this.secret), 'sha512');
|
||||
const signature64 = this.decode (this.stringToBase64 (this.encode (signature)));
|
||||
headers = {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Api-Key': this.apiKey,
|
||||
'Api-Sign': signature64.toString (),
|
||||
'Api-Nonce': nonce,
|
||||
};
|
||||
}
|
||||
return { 'url': url, 'method': method, 'body': body, 'headers': headers };
|
||||
}
|
||||
|
||||
handleErrors (httpCode, reason, url, method, headers, body, response, requestHeaders, requestBody) {
|
||||
if (response === undefined) {
|
||||
return; // fallback to default error handler
|
||||
}
|
||||
if ('status' in response) {
|
||||
//
|
||||
// {"status":"5100","message":"After May 23th, recent_transactions is no longer, hence users will not be able to connect to recent_transactions"}
|
||||
//
|
||||
const status = this.safeString (response, 'status');
|
||||
const message = this.safeString (response, 'message');
|
||||
if (status !== undefined) {
|
||||
if (status === '0000') {
|
||||
return; // no error
|
||||
}
|
||||
const feedback = this.id + ' ' + body;
|
||||
this.throwExactlyMatchedException (this.exceptions, status, feedback);
|
||||
this.throwExactlyMatchedException (this.exceptions, message, feedback);
|
||||
throw new ExchangeError (feedback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async request (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) {
|
||||
const response = await this.fetch2 (path, api, method, params, headers, body);
|
||||
if ('status' in response) {
|
||||
if (response['status'] === '0000') {
|
||||
return response;
|
||||
}
|
||||
throw new ExchangeError (this.id + ' ' + this.json (response));
|
||||
}
|
||||
return response;
|
||||
}
|
||||
};
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
'use strict';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const zb = require ('./zb.js');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
module.exports = class bitkk extends zb {
|
||||
describe () {
|
||||
return this.deepExtend (super.describe (), {
|
||||
'id': 'bitkk',
|
||||
'name': 'bitkk',
|
||||
'comment': 'a Chinese ZB clone',
|
||||
'urls': {
|
||||
'api': {
|
||||
'public': 'http://api.bitkk.com/data', // no https for public API
|
||||
'private': 'https://trade.bitkk.com/api',
|
||||
},
|
||||
'www': 'https://www.bitkk.com',
|
||||
'doc': 'https://www.bitkk.com/i/developer',
|
||||
'fees': 'https://www.bitkk.com/i/rate',
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
+928
@@ -0,0 +1,928 @@
|
||||
'use strict';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const Exchange = require ('./base/Exchange');
|
||||
const { AuthenticationError, ArgumentsRequired, ExchangeError, InvalidOrder, BadRequest, OrderNotFound, DDoSProtection, BadSymbol } = require ('./base/errors');
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
module.exports = class bitmart extends Exchange {
|
||||
describe () {
|
||||
return this.deepExtend (super.describe (), {
|
||||
'id': 'bitmart',
|
||||
'name': 'BitMart',
|
||||
'countries': [ 'US', 'CN', 'HK', 'KR' ],
|
||||
'rateLimit': 1000,
|
||||
'version': 'v2',
|
||||
'has': {
|
||||
'CORS': true,
|
||||
'fetchMarkets': true,
|
||||
'fetchTicker': true,
|
||||
'fetchTickers': true,
|
||||
'fetchTime': true,
|
||||
'fetchCurrencies': true,
|
||||
'fetchOrderBook': true,
|
||||
'fetchTrades': true,
|
||||
'fetchMyTrades': true,
|
||||
'fetchOHLCV': true,
|
||||
'fetchBalance': true,
|
||||
'createOrder': true,
|
||||
'cancelOrder': true,
|
||||
'cancelAllOrders': true,
|
||||
'fetchOrders': false,
|
||||
'fetchOrderTrades': true,
|
||||
'fetchOpenOrders': true,
|
||||
'fetchClosedOrders': true,
|
||||
'fetchCanceledOrders': true,
|
||||
'fetchOrder': true,
|
||||
'signIn': true,
|
||||
},
|
||||
'urls': {
|
||||
'logo': 'https://user-images.githubusercontent.com/1294454/61835713-a2662f80-ae85-11e9-9d00-6442919701fd.jpg',
|
||||
'api': 'https://openapi.bitmart.com',
|
||||
'www': 'https://www.bitmart.com/',
|
||||
'doc': 'https://github.com/bitmartexchange/bitmart-official-api-docs',
|
||||
'referral': 'http://www.bitmart.com/?r=rQCFLh',
|
||||
'fees': 'https://www.bitmart.com/fee/en',
|
||||
},
|
||||
'requiredCredentials': {
|
||||
'apiKey': true,
|
||||
'secret': true,
|
||||
'uid': true,
|
||||
},
|
||||
'api': {
|
||||
'token': {
|
||||
'post': [
|
||||
'authentication',
|
||||
],
|
||||
},
|
||||
'public': {
|
||||
'get': [
|
||||
'currencies',
|
||||
'ping',
|
||||
'steps',
|
||||
'symbols',
|
||||
'symbols_details',
|
||||
'symbols/{symbol}/kline',
|
||||
'symbols/{symbol}/orders',
|
||||
'symbols/{symbol}/trades',
|
||||
'ticker',
|
||||
'time',
|
||||
],
|
||||
},
|
||||
'private': {
|
||||
'get': [
|
||||
'orders',
|
||||
'orders/{id}',
|
||||
'trades',
|
||||
'wallet',
|
||||
],
|
||||
'post': [
|
||||
'orders',
|
||||
],
|
||||
'delete': [
|
||||
'orders',
|
||||
'orders/{id}',
|
||||
],
|
||||
},
|
||||
},
|
||||
'timeframes': {
|
||||
'1m': 1,
|
||||
'3m': 3,
|
||||
'5m': 5,
|
||||
'15m': 15,
|
||||
'30m': 30,
|
||||
'45m': 45,
|
||||
'1h': 60,
|
||||
'2h': 120,
|
||||
'3h': 180,
|
||||
'4h': 240,
|
||||
'1d': 1440,
|
||||
'1w': 10080,
|
||||
'1M': 43200,
|
||||
},
|
||||
'fees': {
|
||||
'trading': {
|
||||
'tierBased': true,
|
||||
'percentage': true,
|
||||
'taker': 0.002,
|
||||
'maker': 0.001,
|
||||
'tiers': {
|
||||
'taker': [
|
||||
[0, 0.20 / 100],
|
||||
[10, 0.18 / 100],
|
||||
[50, 0.16 / 100],
|
||||
[250, 0.14 / 100],
|
||||
[1000, 0.12 / 100],
|
||||
[5000, 0.10 / 100],
|
||||
[25000, 0.08 / 100],
|
||||
[50000, 0.06 / 100],
|
||||
],
|
||||
'maker': [
|
||||
[0, 0.1 / 100],
|
||||
[10, 0.09 / 100],
|
||||
[50, 0.08 / 100],
|
||||
[250, 0.07 / 100],
|
||||
[1000, 0.06 / 100],
|
||||
[5000, 0.05 / 100],
|
||||
[25000, 0.04 / 100],
|
||||
[50000, 0.03 / 100],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
'exceptions': {
|
||||
'exact': {
|
||||
'Place order error': InvalidOrder, // {"message":"Place order error"}
|
||||
'Not found': OrderNotFound, // {"message":"Not found"}
|
||||
'Visit too often, please try again later': DDoSProtection, // {"code":-30,"msg":"Visit too often, please try again later","subMsg":"","data":{}}
|
||||
'Unknown symbol': BadSymbol, // {"message":"Unknown symbol"}
|
||||
},
|
||||
'broad': {
|
||||
'Invalid limit. limit must be in the range': InvalidOrder,
|
||||
'Maximum price is': InvalidOrder, // {"message":"Maximum price is 0.112695"}
|
||||
// {"message":"Required Integer parameter 'status' is not present"}
|
||||
// {"message":"Required String parameter 'symbol' is not present"}
|
||||
// {"message":"Required Integer parameter 'offset' is not present"}
|
||||
// {"message":"Required Integer parameter 'limit' is not present"}
|
||||
// {"message":"Required Long parameter 'from' is not present"}
|
||||
// {"message":"Required Long parameter 'to' is not present"}
|
||||
'is not present': BadRequest,
|
||||
},
|
||||
},
|
||||
'commonCurrencies': {
|
||||
'ONE': 'Menlo One',
|
||||
'PLA': 'Plair',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async fetchTime (params = {}) {
|
||||
const response = await this.publicGetTime (params);
|
||||
//
|
||||
// {
|
||||
// "server_time": 1527777538000
|
||||
// }
|
||||
//
|
||||
return this.safeInteger (response, 'server_time');
|
||||
}
|
||||
|
||||
async signIn (params = {}) {
|
||||
const message = this.apiKey + ':' + this.secret + ':' + this.uid;
|
||||
const data = {
|
||||
'grant_type': 'client_credentials',
|
||||
'client_id': this.apiKey,
|
||||
'client_secret': this.hmac (this.encode (message), this.encode (this.secret), 'sha256'),
|
||||
};
|
||||
const response = await this.tokenPostAuthentication (this.extend (data, params));
|
||||
const accessToken = this.safeString (response, 'access_token');
|
||||
if (!accessToken) {
|
||||
throw new AuthenticationError (this.id + ' signIn() failed to authenticate. Access token missing from response.');
|
||||
}
|
||||
const expiresIn = this.safeInteger (response, 'expires_in');
|
||||
this.options['expires'] = this.sum (this.nonce (), expiresIn * 1000);
|
||||
this.options['accessToken'] = accessToken;
|
||||
return response;
|
||||
}
|
||||
|
||||
async fetchMarkets (params = {}) {
|
||||
const markets = await this.publicGetSymbolsDetails (params);
|
||||
//
|
||||
// [
|
||||
// {
|
||||
// "id":"1SG_BTC",
|
||||
// "base_currency":"1SG",
|
||||
// "quote_currency":"BTC",
|
||||
// "quote_increment":"0.1",
|
||||
// "base_min_size":"0.1000000000",
|
||||
// "base_max_size":"10000000.0000000000",
|
||||
// "price_min_precision":4,
|
||||
// "price_max_precision":6,
|
||||
// "expiration":"NA"
|
||||
// }
|
||||
// ]
|
||||
//
|
||||
const result = [];
|
||||
for (let i = 0; i < markets.length; i++) {
|
||||
const market = markets[i];
|
||||
const id = this.safeString (market, 'id');
|
||||
const baseId = this.safeString (market, 'base_currency');
|
||||
const quoteId = this.safeString (market, 'quote_currency');
|
||||
const base = this.safeCurrencyCode (baseId);
|
||||
const quote = this.safeCurrencyCode (quoteId);
|
||||
const symbol = base + '/' + quote;
|
||||
//
|
||||
// https://github.com/bitmartexchange/bitmart-official-api-docs/blob/master/rest/public/symbols_details.md#response-details
|
||||
// from the above API doc:
|
||||
// quote_increment Minimum order price as well as the price increment
|
||||
// price_min_precision Minimum price precision (digit) used to query price and kline
|
||||
// price_max_precision Maximum price precision (digit) used to query price and kline
|
||||
//
|
||||
// the docs are wrong: https://github.com/ccxt/ccxt/issues/5612
|
||||
//
|
||||
const quoteIncrement = this.safeString (market, 'quote_increment');
|
||||
const amountPrecision = this.precisionFromString (quoteIncrement);
|
||||
const pricePrecision = this.safeInteger (market, 'price_max_precision');
|
||||
const precision = {
|
||||
'amount': amountPrecision,
|
||||
'price': pricePrecision,
|
||||
};
|
||||
const limits = {
|
||||
'amount': {
|
||||
'min': this.safeFloat (market, 'base_min_size'),
|
||||
'max': this.safeFloat (market, 'base_max_size'),
|
||||
},
|
||||
'price': {
|
||||
'min': undefined,
|
||||
'max': undefined,
|
||||
},
|
||||
'cost': {
|
||||
'min': undefined,
|
||||
'max': undefined,
|
||||
},
|
||||
};
|
||||
result.push ({
|
||||
'id': id,
|
||||
'symbol': symbol,
|
||||
'base': base,
|
||||
'quote': quote,
|
||||
'baseId': baseId,
|
||||
'quoteId': quoteId,
|
||||
'precision': precision,
|
||||
'limits': limits,
|
||||
'info': market,
|
||||
'active': undefined,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
parseTicker (ticker, market = undefined) {
|
||||
const timestamp = this.milliseconds ();
|
||||
const marketId = this.safeString (ticker, 'symbol_id');
|
||||
let symbol = undefined;
|
||||
if (marketId !== undefined) {
|
||||
if (marketId in this.markets_by_id) {
|
||||
market = this.markets_by_id[marketId];
|
||||
symbol = market['symbol'];
|
||||
} else if (marketId !== undefined) {
|
||||
const [ baseId, quoteId ] = marketId.split ('_');
|
||||
const base = this.safeCurrencyCode (baseId);
|
||||
const quote = this.safeCurrencyCode (quoteId);
|
||||
symbol = base + '/' + quote;
|
||||
}
|
||||
}
|
||||
const last = this.safeFloat (ticker, 'current_price');
|
||||
const percentage = this.safeFloat (ticker, 'fluctuation');
|
||||
return {
|
||||
'symbol': symbol,
|
||||
'timestamp': timestamp,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'high': this.safeFloat (ticker, 'highest_price'),
|
||||
'low': this.safeFloat (ticker, 'lowest_price'),
|
||||
'bid': this.safeFloat (ticker, 'bid_1'),
|
||||
'bidVolume': this.safeFloat (ticker, 'bid_1_amount'),
|
||||
'ask': this.safeFloat (ticker, 'ask_1'),
|
||||
'askVolume': this.safeFloat (ticker, 'ask_1_amount'),
|
||||
'vwap': undefined,
|
||||
'open': undefined,
|
||||
'close': last,
|
||||
'last': last,
|
||||
'previousClose': undefined,
|
||||
'change': undefined,
|
||||
'percentage': percentage * 100,
|
||||
'average': undefined,
|
||||
'baseVolume': this.safeFloat (ticker, 'volume'),
|
||||
'quoteVolume': this.safeFloat (ticker, 'base_volume'),
|
||||
'info': ticker,
|
||||
};
|
||||
}
|
||||
|
||||
async fetchTicker (symbol, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const request = {
|
||||
'symbol': this.marketId (symbol),
|
||||
};
|
||||
const response = await this.publicGetTicker (this.extend (request, params));
|
||||
//
|
||||
// {
|
||||
// "volume":"97487.38",
|
||||
// "ask_1":"0.00148668",
|
||||
// "base_volume":"144.59",
|
||||
// "lowest_price":"0.00144362",
|
||||
// "bid_1":"0.00148017",
|
||||
// "highest_price":"0.00151000",
|
||||
// "ask_1_amount":"92.03",
|
||||
// "current_price":"0.00148230",
|
||||
// "fluctuation":"+0.0227",
|
||||
// "symbol_id":"XRP_ETH",
|
||||
// "url":"https://www.bitmart.com/trade?symbol=XRP_ETH",
|
||||
// "bid_1_amount":"134.78"
|
||||
// }
|
||||
//
|
||||
return this.parseTicker (response);
|
||||
}
|
||||
|
||||
async fetchTickers (symbols = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const tickers = await this.publicGetTicker (params);
|
||||
const result = {};
|
||||
for (let i = 0; i < tickers.length; i++) {
|
||||
const ticker = this.parseTicker (tickers[i]);
|
||||
const symbol = ticker['symbol'];
|
||||
result[symbol] = ticker;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async fetchCurrencies (params = {}) {
|
||||
const currencies = await this.publicGetCurrencies (params);
|
||||
//
|
||||
// [
|
||||
// {
|
||||
// "name":"CNY1",
|
||||
// "withdraw_enabled":false,
|
||||
// "id":"CNY1",
|
||||
// "deposit_enabled":false
|
||||
// }
|
||||
// ]
|
||||
//
|
||||
const result = {};
|
||||
for (let i = 0; i < currencies.length; i++) {
|
||||
const currency = currencies[i];
|
||||
const currencyId = this.safeString (currency, 'id');
|
||||
const code = this.safeCurrencyCode (currencyId);
|
||||
const name = this.safeString (currency, 'name');
|
||||
const withdrawEnabled = this.safeValue (currency, 'withdraw_enabled');
|
||||
const depositEnabled = this.safeValue (currency, 'deposit_enabled');
|
||||
const active = withdrawEnabled && depositEnabled;
|
||||
result[code] = {
|
||||
'id': currencyId,
|
||||
'code': code,
|
||||
'name': name,
|
||||
'info': currency, // the original payload
|
||||
'active': active,
|
||||
'fee': undefined,
|
||||
'precision': undefined,
|
||||
'limits': {
|
||||
'amount': { 'min': undefined, 'max': undefined },
|
||||
'price': { 'min': undefined, 'max': undefined },
|
||||
'cost': { 'min': undefined, 'max': undefined },
|
||||
'withdraw': { 'min': undefined, 'max': undefined },
|
||||
},
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async fetchOrderBook (symbol, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const request = {
|
||||
'symbol': this.marketId (symbol),
|
||||
// 'precision': 4, // optional price precision / depth level whose range is defined in symbol details
|
||||
};
|
||||
const response = await this.publicGetSymbolsSymbolOrders (this.extend (request, params));
|
||||
return this.parseOrderBook (response, undefined, 'buys', 'sells', 'price', 'amount');
|
||||
}
|
||||
|
||||
parseTrade (trade, market = undefined) {
|
||||
//
|
||||
// fetchTrades (public)
|
||||
//
|
||||
// {
|
||||
// "amount":"2.29275119",
|
||||
// "price":"0.021858",
|
||||
// "count":"104.8930",
|
||||
// "order_time":1563997286061,
|
||||
// "type":"sell"
|
||||
// }
|
||||
//
|
||||
// fetchMyTrades (private)
|
||||
//
|
||||
// {
|
||||
// active: true,
|
||||
// amount: '0.2000',
|
||||
// entrustType: 1,
|
||||
// entrust_id: 979648824,
|
||||
// fees: '0.0000085532',
|
||||
// price: '0.021383',
|
||||
// symbol: 'ETH_BTC',
|
||||
// timestamp: 1574343514000,
|
||||
// trade_id: 329418828
|
||||
// },
|
||||
//
|
||||
const id = this.safeString (trade, 'trade_id');
|
||||
const timestamp = this.safeInteger2 (trade, 'timestamp', 'order_time');
|
||||
const type = undefined;
|
||||
let side = this.safeStringLower (trade, 'type');
|
||||
if ((side === undefined) && ('entrustType' in trade)) {
|
||||
side = trade['entrustType'] ? 'sell' : 'buy';
|
||||
}
|
||||
const price = this.safeFloat (trade, 'price');
|
||||
const amount = this.safeFloat (trade, 'amount');
|
||||
let cost = undefined;
|
||||
if (price !== undefined) {
|
||||
if (amount !== undefined) {
|
||||
cost = amount * price;
|
||||
}
|
||||
}
|
||||
const orderId = this.safeInteger (trade, 'entrust_id');
|
||||
const marketId = this.safeString (trade, 'symbol');
|
||||
let symbol = undefined;
|
||||
if (marketId !== undefined) {
|
||||
if (marketId in this.markets_by_id) {
|
||||
market = this.markets_by_id[marketId];
|
||||
symbol = market['symbol'];
|
||||
} else {
|
||||
const [ baseId, quoteId ] = marketId.split ('_');
|
||||
const base = this.safeCurrencyCode (baseId);
|
||||
const quote = this.safeCurrencyCode (quoteId);
|
||||
symbol = base + '/' + quote;
|
||||
}
|
||||
}
|
||||
if (symbol === undefined) {
|
||||
if (market !== undefined) {
|
||||
symbol = market['symbol'];
|
||||
}
|
||||
}
|
||||
const feeCost = this.safeFloat (trade, 'fees');
|
||||
let fee = undefined;
|
||||
if (feeCost !== undefined) {
|
||||
let feeCurrencyCode = undefined;
|
||||
if (market !== undefined) {
|
||||
feeCurrencyCode = (side === 'buy') ? market['base'] : market['quote'];
|
||||
}
|
||||
fee = {
|
||||
'cost': feeCost,
|
||||
'currency': feeCurrencyCode,
|
||||
};
|
||||
}
|
||||
return {
|
||||
'info': trade,
|
||||
'id': id,
|
||||
'order': orderId,
|
||||
'timestamp': timestamp,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'symbol': symbol,
|
||||
'type': type,
|
||||
'side': side,
|
||||
'price': price,
|
||||
'amount': amount,
|
||||
'cost': cost,
|
||||
'takerOrMaker': undefined,
|
||||
'fee': fee,
|
||||
};
|
||||
}
|
||||
|
||||
async fetchTrades (symbol, since = undefined, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const request = {
|
||||
'symbol': market['id'],
|
||||
};
|
||||
const response = await this.publicGetSymbolsSymbolTrades (this.extend (request, params));
|
||||
//
|
||||
// [
|
||||
// {
|
||||
// "amount":"2.29275119",
|
||||
// "price":"0.021858",
|
||||
// "count":"104.8930",
|
||||
// "order_time":1563997286061,
|
||||
// "type":"sell"
|
||||
// }
|
||||
// ]
|
||||
//
|
||||
return this.parseTrades (response, market, since, limit);
|
||||
}
|
||||
|
||||
async fetchMyTrades (symbol = undefined, since = undefined, limit = undefined, params = {}) {
|
||||
if (symbol === undefined) {
|
||||
throw new ArgumentsRequired (this.id + ' fetchMyTrades requires a symbol argument');
|
||||
}
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
// limit is required, must be in the range (0, 50)
|
||||
const maxLimit = 50;
|
||||
limit = (limit === undefined) ? maxLimit : Math.min (limit, maxLimit);
|
||||
const request = {
|
||||
'symbol': market['id'],
|
||||
'offset': 0, // current page, starts from 0
|
||||
'limit': limit, // required
|
||||
};
|
||||
const response = await this.privateGetTrades (this.extend (request, params));
|
||||
//
|
||||
// {
|
||||
// "total_trades": 216,
|
||||
// "total_pages": 22,
|
||||
// "current_page": 0,
|
||||
// "trades": [
|
||||
// {
|
||||
// "symbol": "BMX_ETH",
|
||||
// "amount": "1.0",
|
||||
// "fees": "0.0005000000",
|
||||
// "trade_id": 2734956,
|
||||
// "price": "0.00013737",
|
||||
// "active": true,
|
||||
// "entrust_id": 5576623,
|
||||
// "timestamp": 1545292334000
|
||||
// },
|
||||
// ]
|
||||
// }
|
||||
//
|
||||
const trades = this.safeValue (response, 'trades', []);
|
||||
return this.parseTrades (trades, market, since, limit);
|
||||
}
|
||||
|
||||
async fetchOrderTrades (id, symbol = undefined, since = undefined, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const request = {
|
||||
'entrust_id': id,
|
||||
};
|
||||
return await this.fetchMyTrades (symbol, since, limit, this.extend (request, params));
|
||||
}
|
||||
|
||||
parseOHLCV (ohlcv, market = undefined, timeframe = '1m', since = undefined, limit = undefined) {
|
||||
return [
|
||||
this.safeInteger (ohlcv, 'timestamp'),
|
||||
this.safeFloat (ohlcv, 'open_price'),
|
||||
this.safeFloat (ohlcv, 'highest_price'),
|
||||
this.safeFloat (ohlcv, 'lowest_price'),
|
||||
this.safeFloat (ohlcv, 'current_price'),
|
||||
this.safeFloat (ohlcv, 'volume'),
|
||||
];
|
||||
}
|
||||
|
||||
async fetchOHLCV (symbol, timeframe = '1m', since = undefined, limit = undefined, params = {}) {
|
||||
if (since === undefined && limit === undefined) {
|
||||
throw new ArgumentsRequired (this.id + ' fetchOHLCV requires either a `since` argument or a `limit` argument (or both)');
|
||||
}
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const periodInSeconds = this.parseTimeframe (timeframe);
|
||||
const duration = periodInSeconds * limit * 1000;
|
||||
let to = this.milliseconds ();
|
||||
if (since === undefined) {
|
||||
since = to - duration;
|
||||
} else {
|
||||
to = this.sum (since, duration);
|
||||
}
|
||||
const request = {
|
||||
'symbol': market['id'],
|
||||
'from': since, // start time of k-line data (in milliseconds, required)
|
||||
'to': to, // end time of k-line data (in milliseconds, required)
|
||||
'step': this.timeframes[timeframe], // steps of sampling (in minutes, default 1 minute, optional)
|
||||
};
|
||||
const response = await this.publicGetSymbolsSymbolKline (this.extend (request, params));
|
||||
//
|
||||
// [
|
||||
// {
|
||||
// "timestamp":1525761000000,
|
||||
// "open_price":"0.010130",
|
||||
// "highest_price":"0.010130",
|
||||
// "lowest_price":"0.010130",
|
||||
// "current_price":"0.010130",
|
||||
// "volume":"0.000000"
|
||||
// }
|
||||
// ]
|
||||
//
|
||||
return this.parseOHLCVs (response, market, timeframe, since, limit);
|
||||
}
|
||||
|
||||
async fetchBalance (params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const balances = await this.privateGetWallet (params);
|
||||
//
|
||||
// [
|
||||
// {
|
||||
// "name":"Bitcoin",
|
||||
// "available":"0.0000000000",
|
||||
// "frozen":"0.0000000000",
|
||||
// "id":"BTC"
|
||||
// }
|
||||
// ]
|
||||
//
|
||||
const result = { 'info': balances };
|
||||
for (let i = 0; i < balances.length; i++) {
|
||||
const balance = balances[i];
|
||||
const currencyId = this.safeString (balance, 'id');
|
||||
const code = this.safeCurrencyCode (currencyId);
|
||||
const account = this.account ();
|
||||
account['free'] = this.safeFloat (balance, 'available');
|
||||
account['used'] = this.safeFloat (balance, 'frozen');
|
||||
result[code] = account;
|
||||
}
|
||||
return this.parseBalance (result);
|
||||
}
|
||||
|
||||
parseOrder (order, market = undefined) {
|
||||
//
|
||||
// createOrder
|
||||
//
|
||||
// {
|
||||
// "entrust_id":1223181
|
||||
// }
|
||||
//
|
||||
// cancelOrder
|
||||
//
|
||||
// {}
|
||||
//
|
||||
// fetchOrder, fetchOrdersByStatus, fetchOpenOrders, fetchClosedOrders
|
||||
//
|
||||
// {
|
||||
// "entrust_id":1223181,
|
||||
// "symbol":"BMX_ETH",
|
||||
// "timestamp":1528060666000,
|
||||
// "side":"buy",
|
||||
// "price":"1.000000",
|
||||
// "fees":"0.1",
|
||||
// "original_amount":"1",
|
||||
// "executed_amount":"1",
|
||||
// "remaining_amount":"0",
|
||||
// "status":3
|
||||
// }
|
||||
//
|
||||
const id = this.safeString (order, 'entrust_id');
|
||||
const timestamp = this.milliseconds ();
|
||||
const status = this.parseOrderStatus (this.safeString (order, 'status'));
|
||||
let symbol = undefined;
|
||||
const marketId = this.safeString (order, 'symbol');
|
||||
if (marketId !== undefined) {
|
||||
if (marketId in this.markets_by_id) {
|
||||
market = this.markets_by_id[marketId];
|
||||
} else {
|
||||
const [ baseId, quoteId ] = marketId.split ('_');
|
||||
const base = this.safeCurrencyCode (baseId);
|
||||
const quote = this.safeCurrencyCode (quoteId);
|
||||
symbol = base + '/' + quote;
|
||||
}
|
||||
}
|
||||
if ((symbol === undefined) && (market !== undefined)) {
|
||||
symbol = market['symbol'];
|
||||
}
|
||||
const price = this.safeFloat (order, 'price');
|
||||
const amount = this.safeFloat (order, 'original_amount');
|
||||
let cost = undefined;
|
||||
let filled = this.safeFloat (order, 'executed_amount');
|
||||
let remaining = this.safeFloat (order, 'remaining_amount');
|
||||
if (amount !== undefined) {
|
||||
if (remaining !== undefined) {
|
||||
if (filled === undefined) {
|
||||
filled = amount - remaining;
|
||||
}
|
||||
}
|
||||
if (filled !== undefined) {
|
||||
if (remaining === undefined) {
|
||||
remaining = amount - filled;
|
||||
}
|
||||
if (cost === undefined) {
|
||||
if (price !== undefined) {
|
||||
cost = price * filled;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const side = this.safeString (order, 'side');
|
||||
const type = undefined;
|
||||
return {
|
||||
'id': id,
|
||||
'clientOrderId': undefined,
|
||||
'info': order,
|
||||
'timestamp': timestamp,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'lastTradeTimestamp': undefined,
|
||||
'symbol': symbol,
|
||||
'type': type,
|
||||
'side': side,
|
||||
'price': price,
|
||||
'amount': amount,
|
||||
'cost': undefined,
|
||||
'average': undefined,
|
||||
'filled': filled,
|
||||
'remaining': remaining,
|
||||
'status': status,
|
||||
'fee': undefined,
|
||||
'trades': undefined,
|
||||
};
|
||||
}
|
||||
|
||||
parseOrderStatus (status) {
|
||||
const statuses = {
|
||||
'0': 'all',
|
||||
'1': 'open',
|
||||
'2': 'open',
|
||||
'3': 'closed',
|
||||
'4': 'canceled',
|
||||
'5': 'open',
|
||||
'6': 'closed',
|
||||
};
|
||||
return this.safeString (statuses, status, status);
|
||||
}
|
||||
|
||||
async createOrder (symbol, type, side, amount, price = undefined, params = {}) {
|
||||
if (type !== 'limit') {
|
||||
throw new ExchangeError (this.id + ' allows limit orders only');
|
||||
}
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const request = {
|
||||
'symbol': market['id'],
|
||||
'side': side.toLowerCase (),
|
||||
'amount': this.amountToPrecision (symbol, amount),
|
||||
'price': this.priceToPrecision (symbol, price),
|
||||
};
|
||||
const response = await this.privatePostOrders (this.extend (request, params));
|
||||
//
|
||||
// {
|
||||
// "entrust_id":1223181
|
||||
// }
|
||||
//
|
||||
return this.parseOrder (response, market);
|
||||
}
|
||||
|
||||
async cancelOrder (id, symbol = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const intId = parseInt (id);
|
||||
const request = {
|
||||
'id': intId,
|
||||
'entrust_id': intId,
|
||||
};
|
||||
const response = await this.privateDeleteOrdersId (this.extend (request, params));
|
||||
//
|
||||
// responds with an empty object {}
|
||||
//
|
||||
return this.parseOrder (response);
|
||||
}
|
||||
|
||||
async cancelAllOrders (symbol = undefined, params = {}) {
|
||||
if (symbol === undefined) {
|
||||
throw new ArgumentsRequired (this.id + ' cancelAllOrders requires a symbol argument');
|
||||
}
|
||||
const side = this.safeString (params, 'side');
|
||||
if (side === undefined) {
|
||||
throw new ArgumentsRequired (this.id + " cancelAllOrders requires a `side` parameter ('buy' or 'sell')");
|
||||
}
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const request = {
|
||||
'symbol': market['id'],
|
||||
'side': side, // 'buy' or 'sell'
|
||||
};
|
||||
const response = await this.privateDeleteOrders (this.extend (request, params));
|
||||
//
|
||||
// responds with an empty object {}
|
||||
//
|
||||
return response;
|
||||
}
|
||||
|
||||
async fetchOrdersByStatus (status, symbol = undefined, since = undefined, limit = undefined, params = {}) {
|
||||
if (symbol === undefined) {
|
||||
throw new ArgumentsRequired (this.id + ' fetchOrdersByStatus requires a symbol argument');
|
||||
}
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
if (limit === undefined) {
|
||||
limit = 500; // default 500, max 1000
|
||||
}
|
||||
const request = {
|
||||
'symbol': market['id'],
|
||||
'status': status,
|
||||
'offset': 0, // current page, starts from 0
|
||||
'limit': limit,
|
||||
};
|
||||
const response = await this.privateGetOrders (this.extend (request, params));
|
||||
//
|
||||
// {
|
||||
// "orders":[
|
||||
// {
|
||||
// "entrust_id":1223181,
|
||||
// "symbol":"BMX_ETH",
|
||||
// "timestamp":1528060666000,
|
||||
// "side":"buy",
|
||||
// "price":"1.000000",
|
||||
// "fees":"0.1",
|
||||
// "original_amount":"1",
|
||||
// "executed_amount":"1",
|
||||
// "remaining_amount":"0",
|
||||
// "status":3
|
||||
// }
|
||||
// ],
|
||||
// "total_pages":1,
|
||||
// "total_orders":1,
|
||||
// "current_page":0,
|
||||
// }
|
||||
//
|
||||
const orders = this.safeValue (response, 'orders', []);
|
||||
return this.parseOrders (orders, market, since, limit);
|
||||
}
|
||||
|
||||
async fetchOpenOrders (symbol = undefined, since = undefined, limit = undefined, params = {}) {
|
||||
// 5 = pending & partially filled orders
|
||||
return await this.fetchOrdersByStatus (5, symbol, since, limit, params);
|
||||
}
|
||||
|
||||
async fetchClosedOrders (symbol = undefined, since = undefined, limit = undefined, params = {}) {
|
||||
// 3 = closed orders
|
||||
return await this.fetchOrdersByStatus (3, symbol, since, limit, params);
|
||||
}
|
||||
|
||||
async fetchCanceledOrders (symbol = undefined, since = undefined, limit = undefined, params = {}) {
|
||||
// 4 = canceled orders
|
||||
return await this.fetchOrdersByStatus (4, symbol, since, limit, params);
|
||||
}
|
||||
|
||||
async fetchOrder (id, symbol = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const request = {
|
||||
'id': id,
|
||||
};
|
||||
const response = await this.privateGetOrdersId (this.extend (request, params));
|
||||
//
|
||||
// {
|
||||
// "entrust_id":1223181,
|
||||
// "symbol":"BMX_ETH",
|
||||
// "timestamp":1528060666000,
|
||||
// "side":"buy",
|
||||
// "price":"1.000000",
|
||||
// "fees":"0.1",
|
||||
// "original_amount":"1",
|
||||
// "executed_amount":"1",
|
||||
// "remaining_amount":"0",
|
||||
// "status":3
|
||||
// }
|
||||
//
|
||||
return this.parseOrder (response);
|
||||
}
|
||||
|
||||
nonce () {
|
||||
return this.milliseconds ();
|
||||
}
|
||||
|
||||
sign (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) {
|
||||
let url = this.urls['api'] + '/' + this.version + '/' + this.implodeParams (path, params);
|
||||
let query = this.omit (params, this.extractParams (path));
|
||||
if (api === 'public') {
|
||||
if (Object.keys (query).length) {
|
||||
url += '?' + this.urlencode (query);
|
||||
}
|
||||
} else if (api === 'token') {
|
||||
this.checkRequiredCredentials ();
|
||||
body = this.urlencode (query);
|
||||
headers = {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
};
|
||||
} else {
|
||||
const nonce = this.nonce ();
|
||||
this.checkRequiredCredentials ();
|
||||
const token = this.safeString (this.options, 'accessToken');
|
||||
if (token === undefined) {
|
||||
throw new AuthenticationError (this.id + ' ' + path + ' endpoint requires an accessToken option or a prior call to signIn() method');
|
||||
}
|
||||
const expires = this.safeInteger (this.options, 'expires');
|
||||
if (expires !== undefined) {
|
||||
if (nonce >= expires) {
|
||||
throw new AuthenticationError (this.id + ' accessToken expired, supply a new accessToken or call the signIn() method');
|
||||
}
|
||||
}
|
||||
if (Object.keys (query).length) {
|
||||
url += '?' + this.urlencode (query);
|
||||
}
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-BM-TIMESTAMP': nonce.toString (),
|
||||
'X-BM-AUTHORIZATION': 'Bearer ' + token,
|
||||
};
|
||||
if (method !== 'GET') {
|
||||
query = this.keysort (query);
|
||||
body = this.json (query);
|
||||
const message = this.urlencode (query);
|
||||
headers['X-BM-SIGNATURE'] = this.hmac (this.encode (message), this.encode (this.secret), 'sha256');
|
||||
}
|
||||
}
|
||||
return { 'url': url, 'method': method, 'body': body, 'headers': headers };
|
||||
}
|
||||
|
||||
handleErrors (code, reason, url, method, headers, body, response, requestHeaders, requestBody) {
|
||||
if (response === undefined) {
|
||||
return;
|
||||
}
|
||||
//
|
||||
// {"message":"Maximum price is 0.112695"}
|
||||
// {"message":"Required Integer parameter 'status' is not present"}
|
||||
// {"message":"Required String parameter 'symbol' is not present"}
|
||||
// {"message":"Required Integer parameter 'offset' is not present"}
|
||||
// {"message":"Required Integer parameter 'limit' is not present"}
|
||||
// {"message":"Required Long parameter 'from' is not present"}
|
||||
// {"message":"Required Long parameter 'to' is not present"}
|
||||
// {"message":"Invalid status. status=6 not support any more, please use 3:deal_success orders, 4:cancelled orders"}
|
||||
// {"message":"Not found"}
|
||||
// {"message":"Place order error"}
|
||||
//
|
||||
const feedback = this.id + ' ' + body;
|
||||
const message = this.safeString2 (response, 'message', 'msg');
|
||||
if (message !== undefined) {
|
||||
this.throwExactlyMatchedException (this.exceptions['exact'], message, feedback);
|
||||
this.throwBroadlyMatchedException (this.exceptions['broad'], message, feedback);
|
||||
throw new ExchangeError (feedback); // unknown message
|
||||
}
|
||||
}
|
||||
};
|
||||
+1188
File diff suppressed because it is too large.
Load diff
+1329
File diff suppressed because it is too large.
Load diff
+567
@@ -0,0 +1,567 @@
|
||||
'use strict';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const Exchange = require ('./base/Exchange');
|
||||
const { ExchangeError, InvalidNonce, AuthenticationError, OrderNotFound } = require ('./base/errors');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
module.exports = class bitso extends Exchange {
|
||||
describe () {
|
||||
return this.deepExtend (super.describe (), {
|
||||
'id': 'bitso',
|
||||
'name': 'Bitso',
|
||||
'countries': [ 'MX' ], // Mexico
|
||||
'rateLimit': 2000, // 30 requests per minute
|
||||
'version': 'v3',
|
||||
'has': {
|
||||
'CORS': false,
|
||||
'fetchMyTrades': true,
|
||||
'fetchOpenOrders': true,
|
||||
'fetchOrder': true,
|
||||
},
|
||||
'urls': {
|
||||
'logo': 'https://user-images.githubusercontent.com/1294454/27766335-715ce7aa-5ed5-11e7-88a8-173a27bb30fe.jpg',
|
||||
'api': 'https://api.bitso.com',
|
||||
'www': 'https://bitso.com',
|
||||
'doc': 'https://bitso.com/api_info',
|
||||
'fees': 'https://bitso.com/fees?l=es',
|
||||
'referral': 'https://bitso.com/?ref=itej',
|
||||
},
|
||||
'options': {
|
||||
'precision': {
|
||||
'XRP': 6,
|
||||
'MXN': 2,
|
||||
'TUSD': 2,
|
||||
},
|
||||
'defaultPrecision': 8,
|
||||
},
|
||||
'api': {
|
||||
'public': {
|
||||
'get': [
|
||||
'available_books',
|
||||
'ticker',
|
||||
'order_book',
|
||||
'trades',
|
||||
],
|
||||
},
|
||||
'private': {
|
||||
'get': [
|
||||
'account_status',
|
||||
'balance',
|
||||
'fees',
|
||||
'fundings',
|
||||
'fundings/{fid}',
|
||||
'funding_destination',
|
||||
'kyc_documents',
|
||||
'ledger',
|
||||
'ledger/trades',
|
||||
'ledger/fees',
|
||||
'ledger/fundings',
|
||||
'ledger/withdrawals',
|
||||
'mx_bank_codes',
|
||||
'open_orders',
|
||||
'order_trades/{oid}',
|
||||
'orders/{oid}',
|
||||
'user_trades',
|
||||
'user_trades/{tid}',
|
||||
'withdrawals/',
|
||||
'withdrawals/{wid}',
|
||||
],
|
||||
'post': [
|
||||
'bitcoin_withdrawal',
|
||||
'debit_card_withdrawal',
|
||||
'ether_withdrawal',
|
||||
'ripple_withdrawal',
|
||||
'bcash_withdrawal',
|
||||
'litecoin_withdrawal',
|
||||
'orders',
|
||||
'phone_number',
|
||||
'phone_verification',
|
||||
'phone_withdrawal',
|
||||
'spei_withdrawal',
|
||||
'ripple_withdrawal',
|
||||
'bcash_withdrawal',
|
||||
'litecoin_withdrawal',
|
||||
],
|
||||
'delete': [
|
||||
'orders/{oid}',
|
||||
'orders/all',
|
||||
],
|
||||
},
|
||||
},
|
||||
'exceptions': {
|
||||
'0201': AuthenticationError, // Invalid Nonce or Invalid Credentials
|
||||
'104': InvalidNonce, // Cannot perform request - nonce must be higher than 1520307203724237
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async fetchMarkets (params = {}) {
|
||||
const response = await this.publicGetAvailableBooks (params);
|
||||
const markets = this.safeValue (response, 'payload');
|
||||
const result = [];
|
||||
for (let i = 0; i < markets.length; i++) {
|
||||
const market = markets[i];
|
||||
const id = this.safeString (market, 'book');
|
||||
const [ baseId, quoteId ] = id.split ('_');
|
||||
let base = baseId.toUpperCase ();
|
||||
let quote = quoteId.toUpperCase ();
|
||||
base = this.safeCurrencyCode (base);
|
||||
quote = this.safeCurrencyCode (quote);
|
||||
const symbol = base + '/' + quote;
|
||||
const limits = {
|
||||
'amount': {
|
||||
'min': this.safeFloat (market, 'minimum_amount'),
|
||||
'max': this.safeFloat (market, 'maximum_amount'),
|
||||
},
|
||||
'price': {
|
||||
'min': this.safeFloat (market, 'minimum_price'),
|
||||
'max': this.safeFloat (market, 'maximum_price'),
|
||||
},
|
||||
'cost': {
|
||||
'min': this.safeFloat (market, 'minimum_value'),
|
||||
'max': this.safeFloat (market, 'maximum_value'),
|
||||
},
|
||||
};
|
||||
const precision = {
|
||||
'amount': this.safeInteger (this.options['precision'], base, this.options['defaultPrecision']),
|
||||
'price': this.safeInteger (this.options['precision'], quote, this.options['defaultPrecision']),
|
||||
};
|
||||
result.push ({
|
||||
'id': id,
|
||||
'symbol': symbol,
|
||||
'base': base,
|
||||
'quote': quote,
|
||||
'baseId': baseId,
|
||||
'quoteId': quoteId,
|
||||
'info': market,
|
||||
'limits': limits,
|
||||
'precision': precision,
|
||||
'active': undefined,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async fetchBalance (params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const response = await this.privateGetBalance (params);
|
||||
const balances = this.safeValue (response['payload'], 'balances');
|
||||
const result = { 'info': response };
|
||||
for (let i = 0; i < balances.length; i++) {
|
||||
const balance = balances[i];
|
||||
const currencyId = this.safeString (balance, 'currency');
|
||||
const code = this.safeCurrencyCode (currencyId);
|
||||
const account = {
|
||||
'free': this.safeFloat (balance, 'available'),
|
||||
'used': this.safeFloat (balance, 'locked'),
|
||||
'total': this.safeFloat (balance, 'total'),
|
||||
};
|
||||
result[code] = account;
|
||||
}
|
||||
return this.parseBalance (result);
|
||||
}
|
||||
|
||||
async fetchOrderBook (symbol, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const request = {
|
||||
'book': this.marketId (symbol),
|
||||
};
|
||||
const response = await this.publicGetOrderBook (this.extend (request, params));
|
||||
const orderbook = this.safeValue (response, 'payload');
|
||||
const timestamp = this.parse8601 (this.safeString (orderbook, 'updated_at'));
|
||||
return this.parseOrderBook (orderbook, timestamp, 'bids', 'asks', 'price', 'amount');
|
||||
}
|
||||
|
||||
async fetchTicker (symbol, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const request = {
|
||||
'book': this.marketId (symbol),
|
||||
};
|
||||
const response = await this.publicGetTicker (this.extend (request, params));
|
||||
const ticker = this.safeValue (response, 'payload');
|
||||
const timestamp = this.parse8601 (this.safeString (ticker, 'created_at'));
|
||||
const vwap = this.safeFloat (ticker, 'vwap');
|
||||
const baseVolume = this.safeFloat (ticker, 'volume');
|
||||
let quoteVolume = undefined;
|
||||
if (baseVolume !== undefined && vwap !== undefined) {
|
||||
quoteVolume = baseVolume * vwap;
|
||||
}
|
||||
const last = this.safeFloat (ticker, 'last');
|
||||
return {
|
||||
'symbol': symbol,
|
||||
'timestamp': timestamp,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'high': this.safeFloat (ticker, 'high'),
|
||||
'low': this.safeFloat (ticker, 'low'),
|
||||
'bid': this.safeFloat (ticker, 'bid'),
|
||||
'bidVolume': undefined,
|
||||
'ask': this.safeFloat (ticker, 'ask'),
|
||||
'askVolume': undefined,
|
||||
'vwap': vwap,
|
||||
'open': undefined,
|
||||
'close': last,
|
||||
'last': last,
|
||||
'previousClose': undefined,
|
||||
'change': undefined,
|
||||
'percentage': undefined,
|
||||
'average': undefined,
|
||||
'baseVolume': baseVolume,
|
||||
'quoteVolume': quoteVolume,
|
||||
'info': ticker,
|
||||
};
|
||||
}
|
||||
|
||||
parseTrade (trade, market = undefined) {
|
||||
const timestamp = this.parse8601 (this.safeString (trade, 'created_at'));
|
||||
let symbol = undefined;
|
||||
if (market === undefined) {
|
||||
const marketId = this.safeString (trade, 'book');
|
||||
if (marketId in this.markets_by_id) {
|
||||
market = this.markets_by_id[marketId];
|
||||
}
|
||||
}
|
||||
if (market !== undefined) {
|
||||
symbol = market['symbol'];
|
||||
}
|
||||
const side = this.safeString2 (trade, 'side', 'maker_side');
|
||||
let amount = this.safeFloat2 (trade, 'amount', 'major');
|
||||
if (amount !== undefined) {
|
||||
amount = Math.abs (amount);
|
||||
}
|
||||
let fee = undefined;
|
||||
const feeCost = this.safeFloat (trade, 'fees_amount');
|
||||
if (feeCost !== undefined) {
|
||||
const feeCurrencyId = this.safeString (trade, 'fees_currency');
|
||||
const feeCurrency = this.safeCurrencyCode (feeCurrencyId);
|
||||
fee = {
|
||||
'cost': feeCost,
|
||||
'currency': feeCurrency,
|
||||
};
|
||||
}
|
||||
let cost = this.safeFloat (trade, 'minor');
|
||||
if (cost !== undefined) {
|
||||
cost = Math.abs (cost);
|
||||
}
|
||||
const price = this.safeFloat (trade, 'price');
|
||||
const orderId = this.safeString (trade, 'oid');
|
||||
const id = this.safeString (trade, 'tid');
|
||||
return {
|
||||
'id': id,
|
||||
'info': trade,
|
||||
'timestamp': timestamp,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'symbol': symbol,
|
||||
'order': orderId,
|
||||
'type': undefined,
|
||||
'side': side,
|
||||
'takerOrMaker': undefined,
|
||||
'price': price,
|
||||
'amount': amount,
|
||||
'cost': cost,
|
||||
'fee': fee,
|
||||
};
|
||||
}
|
||||
|
||||
async fetchTrades (symbol, since = undefined, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const request = {
|
||||
'book': market['id'],
|
||||
};
|
||||
const response = await this.publicGetTrades (this.extend (request, params));
|
||||
return this.parseTrades (response['payload'], market, since, limit);
|
||||
}
|
||||
|
||||
async fetchMyTrades (symbol = undefined, since = undefined, limit = 25, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
// the don't support fetching trades starting from a date yet
|
||||
// use the `marker` extra param for that
|
||||
// this is not a typo, the variable name is 'marker' (don't confuse with 'market')
|
||||
const markerInParams = ('marker' in params);
|
||||
// warn the user with an exception if the user wants to filter
|
||||
// starting from since timestamp, but does not set the trade id with an extra 'marker' param
|
||||
if ((since !== undefined) && !markerInParams) {
|
||||
throw new ExchangeError (this.id + ' fetchMyTrades does not support fetching trades starting from a timestamp with the `since` argument, use the `marker` extra param to filter starting from an integer trade id');
|
||||
}
|
||||
// convert it to an integer unconditionally
|
||||
if (markerInParams) {
|
||||
params = this.extend (params, {
|
||||
'marker': parseInt (params['marker']),
|
||||
});
|
||||
}
|
||||
const request = {
|
||||
'book': market['id'],
|
||||
'limit': limit, // default = 25, max = 100
|
||||
// 'sort': 'desc', // default = desc
|
||||
// 'marker': id, // integer id to start from
|
||||
};
|
||||
const response = await this.privateGetUserTrades (this.extend (request, params));
|
||||
return this.parseTrades (response['payload'], market, since, limit);
|
||||
}
|
||||
|
||||
async createOrder (symbol, type, side, amount, price = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const request = {
|
||||
'book': this.marketId (symbol),
|
||||
'side': side,
|
||||
'type': type,
|
||||
'major': this.amountToPrecision (symbol, amount),
|
||||
};
|
||||
if (type === 'limit') {
|
||||
request['price'] = this.priceToPrecision (symbol, price);
|
||||
}
|
||||
const response = await this.privatePostOrders (this.extend (request, params));
|
||||
const id = this.safeString (response['payload'], 'oid');
|
||||
return {
|
||||
'info': response,
|
||||
'id': id,
|
||||
};
|
||||
}
|
||||
|
||||
async cancelOrder (id, symbol = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const request = {
|
||||
'oid': id,
|
||||
};
|
||||
return await this.privateDeleteOrdersOid (this.extend (request, params));
|
||||
}
|
||||
|
||||
parseOrderStatus (status) {
|
||||
const statuses = {
|
||||
'partial-fill': 'open', // this is a common substitution in ccxt
|
||||
'completed': 'closed',
|
||||
};
|
||||
return this.safeString (statuses, status, status);
|
||||
}
|
||||
|
||||
parseOrder (order, market = undefined) {
|
||||
const id = this.safeString (order, 'oid');
|
||||
const side = this.safeString (order, 'side');
|
||||
const status = this.parseOrderStatus (this.safeString (order, 'status'));
|
||||
let symbol = undefined;
|
||||
const marketId = this.safeString (order, 'book');
|
||||
if (marketId !== undefined) {
|
||||
if (marketId in this.markets_by_id) {
|
||||
market = this.markets_by_id[marketId];
|
||||
} else {
|
||||
const [ baseId, quoteId ] = marketId.split ('_');
|
||||
const base = this.safeCurrencyCode (baseId);
|
||||
const quote = this.safeCurrencyCode (quoteId);
|
||||
symbol = base + '/' + quote;
|
||||
}
|
||||
}
|
||||
if (symbol === undefined) {
|
||||
if (market !== undefined) {
|
||||
symbol = market['symbol'];
|
||||
}
|
||||
}
|
||||
const orderType = this.safeString (order, 'type');
|
||||
const timestamp = this.parse8601 (this.safeString (order, 'created_at'));
|
||||
const price = this.safeFloat (order, 'price');
|
||||
const amount = this.safeFloat (order, 'original_amount');
|
||||
const remaining = this.safeFloat (order, 'unfilled_amount');
|
||||
let filled = undefined;
|
||||
if (amount !== undefined) {
|
||||
if (remaining !== undefined) {
|
||||
filled = amount - remaining;
|
||||
}
|
||||
}
|
||||
const clientOrderId = this.safeString (order, 'client_id');
|
||||
return {
|
||||
'info': order,
|
||||
'id': id,
|
||||
'clientOrderId': clientOrderId,
|
||||
'timestamp': timestamp,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'lastTradeTimestamp': undefined,
|
||||
'symbol': symbol,
|
||||
'type': orderType,
|
||||
'side': side,
|
||||
'price': price,
|
||||
'amount': amount,
|
||||
'cost': undefined,
|
||||
'remaining': remaining,
|
||||
'filled': filled,
|
||||
'status': status,
|
||||
'fee': undefined,
|
||||
'average': undefined,
|
||||
'trades': undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async fetchOpenOrders (symbol = undefined, since = undefined, limit = 25, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
// the don't support fetching trades starting from a date yet
|
||||
// use the `marker` extra param for that
|
||||
// this is not a typo, the variable name is 'marker' (don't confuse with 'market')
|
||||
const markerInParams = ('marker' in params);
|
||||
// warn the user with an exception if the user wants to filter
|
||||
// starting from since timestamp, but does not set the trade id with an extra 'marker' param
|
||||
if ((since !== undefined) && !markerInParams) {
|
||||
throw new ExchangeError (this.id + ' fetchOpenOrders does not support fetching orders starting from a timestamp with the `since` argument, use the `marker` extra param to filter starting from an integer trade id');
|
||||
}
|
||||
// convert it to an integer unconditionally
|
||||
if (markerInParams) {
|
||||
params = this.extend (params, {
|
||||
'marker': parseInt (params['marker']),
|
||||
});
|
||||
}
|
||||
const request = {
|
||||
'book': market['id'],
|
||||
'limit': limit, // default = 25, max = 100
|
||||
// 'sort': 'desc', // default = desc
|
||||
// 'marker': id, // integer id to start from
|
||||
};
|
||||
const response = await this.privateGetOpenOrders (this.extend (request, params));
|
||||
const orders = this.parseOrders (response['payload'], market, since, limit);
|
||||
return orders;
|
||||
}
|
||||
|
||||
async fetchOrder (id, symbol = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const response = await this.privateGetOrdersOid ({
|
||||
'oid': id,
|
||||
});
|
||||
const payload = this.safeValue (response, 'payload');
|
||||
if (Array.isArray (payload)) {
|
||||
const numOrders = response['payload'].length;
|
||||
if (numOrders === 1) {
|
||||
return this.parseOrder (payload[0]);
|
||||
}
|
||||
}
|
||||
throw new OrderNotFound (this.id + ': The order ' + id + ' not found.');
|
||||
}
|
||||
|
||||
async fetchOrderTrades (id, symbol = undefined, since = undefined, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const request = {
|
||||
'oid': id,
|
||||
};
|
||||
const response = await this.privateGetOrderTradesOid (this.extend (request, params));
|
||||
return this.parseTrades (response['payload'], market);
|
||||
}
|
||||
|
||||
async fetchDepositAddress (code, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const currency = this.currency (code);
|
||||
const request = {
|
||||
'fund_currency': currency['id'],
|
||||
};
|
||||
const response = await this.privateGetFundingDestination (this.extend (request, params));
|
||||
let address = this.safeString (response['payload'], 'account_identifier');
|
||||
let tag = undefined;
|
||||
if (code === 'XRP') {
|
||||
const parts = address.split ('?dt=');
|
||||
address = parts[0];
|
||||
tag = parts[1];
|
||||
}
|
||||
this.checkAddress (address);
|
||||
return {
|
||||
'currency': code,
|
||||
'address': address,
|
||||
'tag': tag,
|
||||
'info': response,
|
||||
};
|
||||
}
|
||||
|
||||
async withdraw (code, amount, address, tag = undefined, params = {}) {
|
||||
this.checkAddress (address);
|
||||
await this.loadMarkets ();
|
||||
const methods = {
|
||||
'BTC': 'Bitcoin',
|
||||
'ETH': 'Ether',
|
||||
'XRP': 'Ripple',
|
||||
'BCH': 'Bcash',
|
||||
'LTC': 'Litecoin',
|
||||
};
|
||||
const method = (code in methods) ? methods[code] : undefined;
|
||||
if (method === undefined) {
|
||||
throw new ExchangeError (this.id + ' not valid withdraw coin: ' + code);
|
||||
}
|
||||
const request = {
|
||||
'amount': amount,
|
||||
'address': address,
|
||||
'destination_tag': tag,
|
||||
};
|
||||
const classMethod = 'privatePost' + method + 'Withdrawal';
|
||||
const response = await this[classMethod] (this.extend (request, params));
|
||||
return {
|
||||
'info': response,
|
||||
'id': this.safeString (response['payload'], 'wid'),
|
||||
};
|
||||
}
|
||||
|
||||
sign (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) {
|
||||
let endpoint = '/' + this.version + '/' + this.implodeParams (path, params);
|
||||
const query = this.omit (params, this.extractParams (path));
|
||||
if (method === 'GET') {
|
||||
if (Object.keys (query).length) {
|
||||
endpoint += '?' + this.urlencode (query);
|
||||
}
|
||||
}
|
||||
const url = this.urls['api'] + endpoint;
|
||||
if (api === 'private') {
|
||||
this.checkRequiredCredentials ();
|
||||
const nonce = this.nonce ().toString ();
|
||||
let request = [ nonce, method, endpoint ].join ('');
|
||||
if (method !== 'GET') {
|
||||
if (Object.keys (query).length) {
|
||||
body = this.json (query);
|
||||
request += body;
|
||||
}
|
||||
}
|
||||
const signature = this.hmac (this.encode (request), this.encode (this.secret));
|
||||
const auth = this.apiKey + ':' + nonce + ':' + signature;
|
||||
headers = {
|
||||
'Authorization': 'Bitso ' + auth,
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
}
|
||||
return { 'url': url, 'method': method, 'body': body, 'headers': headers };
|
||||
}
|
||||
|
||||
handleErrors (httpCode, reason, url, method, headers, body, response, requestHeaders, requestBody) {
|
||||
if (response === undefined) {
|
||||
return; // fallback to default error handler
|
||||
}
|
||||
if ('success' in response) {
|
||||
//
|
||||
// {"success":false,"error":{"code":104,"message":"Cannot perform request - nonce must be higher than 1520307203724237"}}
|
||||
//
|
||||
let success = this.safeValue (response, 'success', false);
|
||||
if (typeof success === 'string') {
|
||||
if ((success === 'true') || (success === '1')) {
|
||||
success = true;
|
||||
} else {
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
if (!success) {
|
||||
const feedback = this.id + ' ' + this.json (response);
|
||||
const error = this.safeValue (response, 'error');
|
||||
if (error === undefined) {
|
||||
throw new ExchangeError (feedback);
|
||||
}
|
||||
const code = this.safeString (error, 'code');
|
||||
this.throwExactlyMatchedException (this.exceptions, code, feedback);
|
||||
throw new ExchangeError (feedback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async request (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) {
|
||||
const response = await this.fetch2 (path, api, method, params, headers, body);
|
||||
if ('success' in response) {
|
||||
if (response['success']) {
|
||||
return response;
|
||||
}
|
||||
}
|
||||
throw new ExchangeError (this.id + ' ' + this.json (response));
|
||||
}
|
||||
};
|
||||
+1204
File diff suppressed because it is too large.
Load diff
+291
@@ -0,0 +1,291 @@
|
||||
'use strict';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const Exchange = require ('./base/Exchange');
|
||||
const { BadSymbol, ExchangeError, NotSupported } = require ('./base/errors');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
module.exports = class bitstamp1 extends Exchange {
|
||||
describe () {
|
||||
return this.deepExtend (super.describe (), {
|
||||
'id': 'bitstamp1',
|
||||
'name': 'Bitstamp',
|
||||
'countries': [ 'GB' ],
|
||||
'rateLimit': 1000,
|
||||
'version': 'v1',
|
||||
'has': {
|
||||
'CORS': true,
|
||||
},
|
||||
'urls': {
|
||||
'logo': 'https://user-images.githubusercontent.com/1294454/27786377-8c8ab57e-5fe9-11e7-8ea4-2b05b6bcceec.jpg',
|
||||
'api': 'https://www.bitstamp.net/api',
|
||||
'www': 'https://www.bitstamp.net',
|
||||
'doc': 'https://www.bitstamp.net/api',
|
||||
},
|
||||
'requiredCredentials': {
|
||||
'apiKey': true,
|
||||
'secret': true,
|
||||
'uid': true,
|
||||
},
|
||||
'api': {
|
||||
'public': {
|
||||
'get': [
|
||||
'ticker',
|
||||
'ticker_hour',
|
||||
'order_book',
|
||||
'transactions',
|
||||
'eur_usd',
|
||||
],
|
||||
},
|
||||
'private': {
|
||||
'post': [
|
||||
'balance',
|
||||
'user_transactions',
|
||||
'open_orders',
|
||||
'order_status',
|
||||
'cancel_order',
|
||||
'cancel_all_orders',
|
||||
'buy',
|
||||
'sell',
|
||||
'bitcoin_deposit_address',
|
||||
'unconfirmed_btc',
|
||||
'ripple_withdrawal',
|
||||
'ripple_address',
|
||||
'withdrawal_requests',
|
||||
'bitcoin_withdrawal',
|
||||
],
|
||||
},
|
||||
},
|
||||
'markets': {
|
||||
'BTC/USD': { 'id': 'btcusd', 'symbol': 'BTC/USD', 'base': 'BTC', 'quote': 'USD', 'baseId': 'btc', 'quoteId': 'usd', 'maker': 0.0025, 'taker': 0.0025 },
|
||||
'BTC/EUR': { 'id': 'btceur', 'symbol': 'BTC/EUR', 'base': 'BTC', 'quote': 'EUR', 'baseId': 'btc', 'quoteId': 'eur', 'maker': 0.0025, 'taker': 0.0025 },
|
||||
'EUR/USD': { 'id': 'eurusd', 'symbol': 'EUR/USD', 'base': 'EUR', 'quote': 'USD', 'baseId': 'eur', 'quoteId': 'usd', 'maker': 0.0025, 'taker': 0.0025 },
|
||||
'XRP/USD': { 'id': 'xrpusd', 'symbol': 'XRP/USD', 'base': 'XRP', 'quote': 'USD', 'baseId': 'xrp', 'quoteId': 'usd', 'maker': 0.0025, 'taker': 0.0025 },
|
||||
'XRP/EUR': { 'id': 'xrpeur', 'symbol': 'XRP/EUR', 'base': 'XRP', 'quote': 'EUR', 'baseId': 'xrp', 'quoteId': 'eur', 'maker': 0.0025, 'taker': 0.0025 },
|
||||
'XRP/BTC': { 'id': 'xrpbtc', 'symbol': 'XRP/BTC', 'base': 'XRP', 'quote': 'BTC', 'baseId': 'xrp', 'quoteId': 'btc', 'maker': 0.0025, 'taker': 0.0025 },
|
||||
'LTC/USD': { 'id': 'ltcusd', 'symbol': 'LTC/USD', 'base': 'LTC', 'quote': 'USD', 'baseId': 'ltc', 'quoteId': 'usd', 'maker': 0.0025, 'taker': 0.0025 },
|
||||
'LTC/EUR': { 'id': 'ltceur', 'symbol': 'LTC/EUR', 'base': 'LTC', 'quote': 'EUR', 'baseId': 'ltc', 'quoteId': 'eur', 'maker': 0.0025, 'taker': 0.0025 },
|
||||
'LTC/BTC': { 'id': 'ltcbtc', 'symbol': 'LTC/BTC', 'base': 'LTC', 'quote': 'BTC', 'baseId': 'ltc', 'quoteId': 'btc', 'maker': 0.0025, 'taker': 0.0025 },
|
||||
'ETH/USD': { 'id': 'ethusd', 'symbol': 'ETH/USD', 'base': 'ETH', 'quote': 'USD', 'baseId': 'eth', 'quoteId': 'usd', 'maker': 0.0025, 'taker': 0.0025 },
|
||||
'ETH/EUR': { 'id': 'etheur', 'symbol': 'ETH/EUR', 'base': 'ETH', 'quote': 'EUR', 'baseId': 'eth', 'quoteId': 'eur', 'maker': 0.0025, 'taker': 0.0025 },
|
||||
'ETH/BTC': { 'id': 'ethbtc', 'symbol': 'ETH/BTC', 'base': 'ETH', 'quote': 'BTC', 'baseId': 'eth', 'quoteId': 'btc', 'maker': 0.0025, 'taker': 0.0025 },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async fetchOrderBook (symbol, limit = undefined, params = {}) {
|
||||
if (symbol !== 'BTC/USD') {
|
||||
throw new ExchangeError (this.id + ' ' + this.version + " fetchOrderBook doesn't support " + symbol + ', use it for BTC/USD only');
|
||||
}
|
||||
await this.loadMarkets ();
|
||||
const orderbook = await this.publicGetOrderBook (params);
|
||||
const timestamp = this.safeTimestamp (orderbook, 'timestamp');
|
||||
return this.parseOrderBook (orderbook, timestamp);
|
||||
}
|
||||
|
||||
async fetchTicker (symbol, params = {}) {
|
||||
if (symbol !== 'BTC/USD') {
|
||||
throw new ExchangeError (this.id + ' ' + this.version + " fetchTicker doesn't support " + symbol + ', use it for BTC/USD only');
|
||||
}
|
||||
await this.loadMarkets ();
|
||||
const ticker = await this.publicGetTicker (params);
|
||||
const timestamp = this.safeTimestamp (ticker, 'timestamp');
|
||||
const vwap = this.safeFloat (ticker, 'vwap');
|
||||
const baseVolume = this.safeFloat (ticker, 'volume');
|
||||
let quoteVolume = undefined;
|
||||
if (baseVolume !== undefined && vwap !== undefined) {
|
||||
quoteVolume = baseVolume * vwap;
|
||||
}
|
||||
const last = this.safeFloat (ticker, 'last');
|
||||
return {
|
||||
'symbol': symbol,
|
||||
'timestamp': timestamp,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'high': this.safeFloat (ticker, 'high'),
|
||||
'low': this.safeFloat (ticker, 'low'),
|
||||
'bid': this.safeFloat (ticker, 'bid'),
|
||||
'bidVolume': undefined,
|
||||
'ask': this.safeFloat (ticker, 'ask'),
|
||||
'askVolume': undefined,
|
||||
'vwap': vwap,
|
||||
'open': this.safeFloat (ticker, 'open'),
|
||||
'close': last,
|
||||
'last': last,
|
||||
'previousClose': undefined,
|
||||
'change': undefined,
|
||||
'percentage': undefined,
|
||||
'average': undefined,
|
||||
'baseVolume': baseVolume,
|
||||
'quoteVolume': quoteVolume,
|
||||
'info': ticker,
|
||||
};
|
||||
}
|
||||
|
||||
parseTrade (trade, market = undefined) {
|
||||
const timestamp = this.safeTimestamp2 (trade, 'date', 'datetime');
|
||||
const side = (trade['type'] === 0) ? 'buy' : 'sell';
|
||||
const orderId = this.safeString (trade, 'order_id');
|
||||
if ('currency_pair' in trade) {
|
||||
if (trade['currency_pair'] in this.markets_by_id) {
|
||||
market = this.markets_by_id[trade['currency_pair']];
|
||||
}
|
||||
}
|
||||
const id = this.safeString (trade, 'tid');
|
||||
const price = this.safeFloat (trade, 'price');
|
||||
const amount = this.safeFloat (trade, 'amount');
|
||||
let cost = undefined;
|
||||
if (price !== undefined) {
|
||||
if (amount !== undefined) {
|
||||
cost = price * amount;
|
||||
}
|
||||
}
|
||||
let symbol = undefined;
|
||||
if (market !== undefined) {
|
||||
symbol = market['symbol'];
|
||||
}
|
||||
return {
|
||||
'id': id,
|
||||
'info': trade,
|
||||
'timestamp': timestamp,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'symbol': symbol,
|
||||
'order': orderId,
|
||||
'type': undefined,
|
||||
'side': side,
|
||||
'takerOrMaker': undefined,
|
||||
'price': price,
|
||||
'amount': amount,
|
||||
'cost': cost,
|
||||
'fee': undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async fetchTrades (symbol, since = undefined, limit = undefined, params = {}) {
|
||||
if (symbol !== 'BTC/USD') {
|
||||
throw new BadSymbol (this.id + ' ' + this.version + " fetchTrades doesn't support " + symbol + ', use it for BTC/USD only');
|
||||
}
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const request = {
|
||||
'time': 'minute',
|
||||
};
|
||||
const response = await this.publicGetTransactions (this.extend (request, params));
|
||||
return this.parseTrades (response, market, since, limit);
|
||||
}
|
||||
|
||||
async fetchBalance (params = {}) {
|
||||
const balance = await this.privatePostBalance (params);
|
||||
const result = { 'info': balance };
|
||||
const codes = Object.keys (this.currencies);
|
||||
for (let i = 0; i < codes.length; i++) {
|
||||
const code = codes[i];
|
||||
const currency = this.currency (code);
|
||||
const currencyId = currency['id'];
|
||||
const account = this.account ();
|
||||
account['free'] = this.safeFloat (balance, currencyId + '_available');
|
||||
account['used'] = this.safeFloat (balance, currencyId + '_reserved');
|
||||
account['total'] = this.safeFloat (balance, currencyId + '_balance');
|
||||
result[code] = account;
|
||||
}
|
||||
return this.parseBalance (result);
|
||||
}
|
||||
|
||||
async createOrder (symbol, type, side, amount, price = undefined, params = {}) {
|
||||
if (type !== 'limit') {
|
||||
throw new ExchangeError (this.id + ' ' + this.version + ' accepts limit orders only');
|
||||
}
|
||||
if (symbol !== 'BTC/USD') {
|
||||
throw new ExchangeError (this.id + ' v1 supports BTC/USD orders only');
|
||||
}
|
||||
await this.loadMarkets ();
|
||||
const method = 'privatePost' + this.capitalize (side);
|
||||
const request = {
|
||||
'amount': amount,
|
||||
'price': price,
|
||||
};
|
||||
const response = await this[method] (this.extend (request, params));
|
||||
const id = this.safeString (response, 'id');
|
||||
return {
|
||||
'info': response,
|
||||
'id': id,
|
||||
};
|
||||
}
|
||||
|
||||
async cancelOrder (id, symbol = undefined, params = {}) {
|
||||
return await this.privatePostCancelOrder ({ 'id': id });
|
||||
}
|
||||
|
||||
parseOrderStatus (status) {
|
||||
const statuses = {
|
||||
'In Queue': 'open',
|
||||
'Open': 'open',
|
||||
'Finished': 'closed',
|
||||
'Canceled': 'canceled',
|
||||
};
|
||||
return this.safeString (statuses, status, status);
|
||||
}
|
||||
|
||||
async fetchOrderStatus (id, symbol = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const request = {
|
||||
'id': id,
|
||||
};
|
||||
const response = await this.privatePostOrderStatus (this.extend (request, params));
|
||||
return this.parseOrderStatus (response);
|
||||
}
|
||||
|
||||
async fetchMyTrades (symbol = undefined, since = undefined, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
let market = undefined;
|
||||
if (symbol !== undefined) {
|
||||
market = this.market (symbol);
|
||||
}
|
||||
const pair = market ? market['id'] : 'all';
|
||||
const request = {
|
||||
'id': pair,
|
||||
};
|
||||
const response = await this.privatePostOpenOrdersId (this.extend (request, params));
|
||||
return this.parseTrades (response, market, since, limit);
|
||||
}
|
||||
|
||||
async fetchOrder (id, symbol = undefined, params = {}) {
|
||||
throw new NotSupported (this.id + ' fetchOrder is not implemented yet');
|
||||
}
|
||||
|
||||
sign (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) {
|
||||
let url = this.urls['api'] + '/' + this.implodeParams (path, params);
|
||||
let query = this.omit (params, this.extractParams (path));
|
||||
if (api === 'public') {
|
||||
if (Object.keys (query).length) {
|
||||
url += '?' + this.urlencode (query);
|
||||
}
|
||||
} else {
|
||||
this.checkRequiredCredentials ();
|
||||
const nonce = this.nonce ().toString ();
|
||||
const auth = nonce + this.uid + this.apiKey;
|
||||
const signature = this.encode (this.hmac (this.encode (auth), this.encode (this.secret)));
|
||||
query = this.extend ({
|
||||
'key': this.apiKey,
|
||||
'signature': signature.toUpperCase (),
|
||||
'nonce': nonce,
|
||||
}, query);
|
||||
body = this.urlencode (query);
|
||||
headers = {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
};
|
||||
}
|
||||
return { 'url': url, 'method': method, 'body': body, 'headers': headers };
|
||||
}
|
||||
|
||||
async request (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) {
|
||||
const response = await this.fetch2 (path, api, method, params, headers, body);
|
||||
if ('status' in response) {
|
||||
if (response['status'] === 'error') {
|
||||
throw new ExchangeError (this.id + ' ' + this.json (response));
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
};
|
||||
+1540
File diff suppressed because it is too large.
Load diff
+1214
File diff suppressed because it is too large.
Load diff
+233
@@ -0,0 +1,233 @@
|
||||
'use strict';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const Exchange = require ('./base/Exchange');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
module.exports = class bl3p extends Exchange {
|
||||
describe () {
|
||||
return this.deepExtend (super.describe (), {
|
||||
'id': 'bl3p',
|
||||
'name': 'BL3P',
|
||||
'countries': [ 'NL', 'EU' ], // Netherlands, EU
|
||||
'rateLimit': 1000,
|
||||
'version': '1',
|
||||
'comment': 'An exchange market by BitonicNL',
|
||||
'has': {
|
||||
'CORS': false,
|
||||
},
|
||||
'urls': {
|
||||
'logo': 'https://user-images.githubusercontent.com/1294454/28501752-60c21b82-6feb-11e7-818b-055ee6d0e754.jpg',
|
||||
'api': 'https://api.bl3p.eu',
|
||||
'www': [
|
||||
'https://bl3p.eu',
|
||||
'https://bitonic.nl',
|
||||
],
|
||||
'doc': [
|
||||
'https://github.com/BitonicNL/bl3p-api/tree/master/docs',
|
||||
'https://bl3p.eu/api',
|
||||
'https://bitonic.nl/en/api',
|
||||
],
|
||||
},
|
||||
'api': {
|
||||
'public': {
|
||||
'get': [
|
||||
'{market}/ticker',
|
||||
'{market}/orderbook',
|
||||
'{market}/trades',
|
||||
],
|
||||
},
|
||||
'private': {
|
||||
'post': [
|
||||
'{market}/money/depth/full',
|
||||
'{market}/money/order/add',
|
||||
'{market}/money/order/cancel',
|
||||
'{market}/money/order/result',
|
||||
'{market}/money/orders',
|
||||
'{market}/money/orders/history',
|
||||
'{market}/money/trades/fetch',
|
||||
'GENMKT/money/info',
|
||||
'GENMKT/money/deposit_address',
|
||||
'GENMKT/money/new_deposit_address',
|
||||
'GENMKT/money/wallet/history',
|
||||
'GENMKT/money/withdraw',
|
||||
],
|
||||
},
|
||||
},
|
||||
'markets': {
|
||||
'BTC/EUR': { 'id': 'BTCEUR', 'symbol': 'BTC/EUR', 'base': 'BTC', 'quote': 'EUR', 'baseId': 'BTC', 'quoteId': 'EUR', 'maker': 0.0025, 'taker': 0.0025 },
|
||||
'LTC/EUR': { 'id': 'LTCEUR', 'symbol': 'LTC/EUR', 'base': 'LTC', 'quote': 'EUR', 'baseId': 'LTC', 'quoteId': 'EUR', 'maker': 0.0025, 'taker': 0.0025 },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async fetchBalance (params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const response = await this.privatePostGENMKTMoneyInfo (params);
|
||||
const data = this.safeValue (response, 'data', {});
|
||||
const wallets = this.safeValue (data, 'wallets');
|
||||
const result = { 'info': data };
|
||||
const codes = Object.keys (this.currencies);
|
||||
for (let i = 0; i < codes.length; i++) {
|
||||
const code = codes[i];
|
||||
const currency = this.currency (code);
|
||||
const currencyId = currency['id'];
|
||||
const wallet = this.safeValue (wallets, currencyId, {});
|
||||
const available = this.safeValue (wallet, 'available', {});
|
||||
const balance = this.safeValue (wallet, 'balance', {});
|
||||
const account = this.account ();
|
||||
account['free'] = this.safeFloat (available, 'value');
|
||||
account['total'] = this.safeFloat (balance, 'value');
|
||||
result[code] = account;
|
||||
}
|
||||
return this.parseBalance (result);
|
||||
}
|
||||
|
||||
parseBidAsk (bidask, priceKey = 0, amountKey = 1) {
|
||||
return [
|
||||
bidask[priceKey] / 100000.0,
|
||||
bidask[amountKey] / 100000000.0,
|
||||
];
|
||||
}
|
||||
|
||||
async fetchOrderBook (symbol, limit = undefined, params = {}) {
|
||||
const market = this.market (symbol);
|
||||
const request = {
|
||||
'market': market['id'],
|
||||
};
|
||||
const response = await this.publicGetMarketOrderbook (this.extend (request, params));
|
||||
const orderbook = this.safeValue (response, 'data');
|
||||
return this.parseOrderBook (orderbook, undefined, 'bids', 'asks', 'price_int', 'amount_int');
|
||||
}
|
||||
|
||||
async fetchTicker (symbol, params = {}) {
|
||||
const request = {
|
||||
'market': this.marketId (symbol),
|
||||
};
|
||||
const ticker = await this.publicGetMarketTicker (this.extend (request, params));
|
||||
const timestamp = this.safeTimestamp (ticker, 'timestamp');
|
||||
const last = this.safeFloat (ticker, 'last');
|
||||
return {
|
||||
'symbol': symbol,
|
||||
'timestamp': timestamp,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'high': this.safeFloat (ticker, 'high'),
|
||||
'low': this.safeFloat (ticker, 'low'),
|
||||
'bid': this.safeFloat (ticker, 'bid'),
|
||||
'bidVolume': undefined,
|
||||
'ask': this.safeFloat (ticker, 'ask'),
|
||||
'askVolume': undefined,
|
||||
'vwap': undefined,
|
||||
'open': undefined,
|
||||
'close': last,
|
||||
'last': last,
|
||||
'previousClose': undefined,
|
||||
'change': undefined,
|
||||
'percentage': undefined,
|
||||
'average': undefined,
|
||||
'baseVolume': this.safeFloat (ticker['volume'], '24h'),
|
||||
'quoteVolume': undefined,
|
||||
'info': ticker,
|
||||
};
|
||||
}
|
||||
|
||||
parseTrade (trade, market = undefined) {
|
||||
const id = this.safeString (trade, 'trade_id');
|
||||
const timestamp = this.safeInteger (trade, 'date');
|
||||
let price = this.safeFloat (trade, 'price_int');
|
||||
if (price !== undefined) {
|
||||
price /= 100000.0;
|
||||
}
|
||||
let amount = this.safeFloat (trade, 'amount_int');
|
||||
if (amount !== undefined) {
|
||||
amount /= 100000000.0;
|
||||
}
|
||||
let cost = undefined;
|
||||
if (price !== undefined) {
|
||||
if (amount !== undefined) {
|
||||
cost = amount * price;
|
||||
}
|
||||
}
|
||||
let symbol = undefined;
|
||||
if (market !== undefined) {
|
||||
symbol = market['symbol'];
|
||||
}
|
||||
return {
|
||||
'id': id,
|
||||
'info': trade,
|
||||
'timestamp': timestamp,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'symbol': symbol,
|
||||
'type': undefined,
|
||||
'side': undefined,
|
||||
'order': undefined,
|
||||
'takerOrMaker': undefined,
|
||||
'price': price,
|
||||
'amount': amount,
|
||||
'cost': cost,
|
||||
'fee': undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async fetchTrades (symbol, since = undefined, limit = undefined, params = {}) {
|
||||
const market = this.market (symbol);
|
||||
const response = await this.publicGetMarketTrades (this.extend ({
|
||||
'market': market['id'],
|
||||
}, params));
|
||||
const result = this.parseTrades (response['data']['trades'], market, since, limit);
|
||||
return result;
|
||||
}
|
||||
|
||||
async createOrder (symbol, type, side, amount, price = undefined, params = {}) {
|
||||
const market = this.market (symbol);
|
||||
const order = {
|
||||
'market': market['id'],
|
||||
'amount_int': parseInt (amount * 100000000),
|
||||
'fee_currency': market['quote'],
|
||||
'type': (side === 'buy') ? 'bid' : 'ask',
|
||||
};
|
||||
if (type === 'limit') {
|
||||
order['price_int'] = parseInt (price * 100000.0);
|
||||
}
|
||||
const response = await this.privatePostMarketMoneyOrderAdd (this.extend (order, params));
|
||||
const orderId = this.safeString (response['data'], 'order_id');
|
||||
return {
|
||||
'info': response,
|
||||
'id': orderId,
|
||||
};
|
||||
}
|
||||
|
||||
async cancelOrder (id, symbol = undefined, params = {}) {
|
||||
const request = {
|
||||
'order_id': id,
|
||||
};
|
||||
return await this.privatePostMarketMoneyOrderCancel (this.extend (request, params));
|
||||
}
|
||||
|
||||
sign (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) {
|
||||
const request = this.implodeParams (path, params);
|
||||
let url = this.urls['api'] + '/' + this.version + '/' + request;
|
||||
const query = this.omit (params, this.extractParams (path));
|
||||
if (api === 'public') {
|
||||
if (Object.keys (query).length) {
|
||||
url += '?' + this.urlencode (query);
|
||||
}
|
||||
} else {
|
||||
this.checkRequiredCredentials ();
|
||||
const nonce = this.nonce ();
|
||||
body = this.urlencode (this.extend ({ 'nonce': nonce }, query));
|
||||
const secret = this.base64ToBinary (this.secret);
|
||||
// eslint-disable-next-line quotes
|
||||
const auth = request + "\0" + body;
|
||||
const signature = this.hmac (this.encode (auth), secret, 'sha512', 'base64');
|
||||
headers = {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Rest-Key': this.apiKey,
|
||||
'Rest-Sign': this.decode (signature),
|
||||
};
|
||||
}
|
||||
return { 'url': url, 'method': method, 'body': body, 'headers': headers };
|
||||
}
|
||||
};
|
||||
+839
@@ -0,0 +1,839 @@
|
||||
'use strict';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
const Exchange = require ('./base/Exchange');
|
||||
const { ExchangeError, InvalidOrder, AuthenticationError, InsufficientFunds, BadRequest } = require ('./base/errors');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
module.exports = class bleutrade extends Exchange {
|
||||
describe () {
|
||||
return this.deepExtend (super.describe (), {
|
||||
'id': 'bleutrade',
|
||||
'name': 'Bleutrade',
|
||||
'countries': ['BR'], // Brazil
|
||||
'rateLimit': 1000,
|
||||
'certified': false,
|
||||
'has': {
|
||||
'CORS': true,
|
||||
'cancelOrder': false, // todo
|
||||
'createLimitOrder': false, // todo
|
||||
'createMarketOrder': false, // todo
|
||||
'createOrder': false, // todo
|
||||
'editOrder': false, // todo
|
||||
'withdraw': false, // todo
|
||||
'fetchTrades': false,
|
||||
'fetchTickers': true,
|
||||
'fetchTicker': true,
|
||||
'fetchOrders': false,
|
||||
'fetchClosedOrders': true,
|
||||
'fetchOpenOrders': true,
|
||||
'fetchWithdrawals': true,
|
||||
'fetchOrderTrades': false,
|
||||
'fetchLedger': true,
|
||||
'fetchDepositAddress': true,
|
||||
},
|
||||
'timeframes': {
|
||||
'15m': '15m',
|
||||
'20m': '20m',
|
||||
'30m': '30m',
|
||||
'1h': '1h',
|
||||
'2h': '2h',
|
||||
'3h': '3h',
|
||||
'4h': '4h',
|
||||
'6h': '6h',
|
||||
'8h': '8h',
|
||||
'12h': '12h',
|
||||
'1d': '1d',
|
||||
},
|
||||
'hostname': 'bleutrade.com',
|
||||
'urls': {
|
||||
'logo': 'https://user-images.githubusercontent.com/1294454/30303000-b602dbe6-976d-11e7-956d-36c5049c01e7.jpg',
|
||||
'api': {
|
||||
'v3Private': 'https://{hostname}/api/v3/private',
|
||||
'v3Public': 'https://{hostname}/api/v3/public',
|
||||
},
|
||||
'www': ['https://bleutrade.com'],
|
||||
'doc': [
|
||||
'https://app.swaggerhub.com/apis-docs/bleu/white-label/3.0.0',
|
||||
],
|
||||
'fees': 'https://bleutrade.com/fees/',
|
||||
},
|
||||
'api': {
|
||||
'v3Public': {
|
||||
'get': [
|
||||
'getassets',
|
||||
'getmarkets',
|
||||
'getticker',
|
||||
'getmarketsummary',
|
||||
'getmarketsummaries',
|
||||
'getorderbook',
|
||||
'getmarkethistory',
|
||||
'getcandles',
|
||||
],
|
||||
},
|
||||
'v3Private': {
|
||||
'get': [
|
||||
'statement',
|
||||
],
|
||||
'post': [
|
||||
'getbalance',
|
||||
'getbalances',
|
||||
'buylimit',
|
||||
'selllimit',
|
||||
'buylimitami',
|
||||
'selllimitami',
|
||||
'buystoplimit',
|
||||
'sellstoplimit',
|
||||
'ordercancel',
|
||||
'getopenorders',
|
||||
'getcloseorders',
|
||||
'getdeposithistory',
|
||||
'getdepositaddress',
|
||||
'getmytransactions',
|
||||
'withdraw',
|
||||
'directtransfer',
|
||||
'getwithdrawhistory',
|
||||
'getlimits',
|
||||
],
|
||||
},
|
||||
},
|
||||
'commonCurrencies': {
|
||||
'EPC': 'Epacoin',
|
||||
},
|
||||
'exceptions': {
|
||||
'exact': {
|
||||
'ERR_INSUFICIENT_BALANCE': InsufficientFunds,
|
||||
'ERR_LOW_VOLUME': BadRequest,
|
||||
},
|
||||
'broad': {
|
||||
'Order is not open': InvalidOrder,
|
||||
'Invalid Account / Api KEY / Api Secret': AuthenticationError, // also happens when an invalid nonce is used
|
||||
},
|
||||
},
|
||||
'fees': {
|
||||
'trading': {
|
||||
'tierBased': false,
|
||||
'percentage': true,
|
||||
'taker': 0.25 / 100,
|
||||
'maker': 0.25 / 100,
|
||||
},
|
||||
},
|
||||
'options': {
|
||||
'parseOrderStatus': true,
|
||||
'symbolSeparator': '_',
|
||||
},
|
||||
});
|
||||
// undocumented api calls
|
||||
// https://bleutrade.com/api/v3/public/tradingview/symbols?symbol=ETH_BTC
|
||||
// https://bleutrade.com/api/v3/public/tradingview/config
|
||||
// https://bleutrade.com/api/v3/public/tradingview/time
|
||||
// https://bleutrade.com/api/v3/private/getcloseorders?market=ETH_BTC
|
||||
// https://bleutrade.com/config contains the fees
|
||||
}
|
||||
|
||||
async fetchCurrencies (params = {}) {
|
||||
const response = await this.v3PublicGetGetassets (params);
|
||||
const items = response['result'];
|
||||
const result = {};
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
// { Asset: 'USDT',
|
||||
// AssetLong: 'Tether',
|
||||
// MinConfirmation: 4,
|
||||
// WithdrawTxFee: 1,
|
||||
// WithdrawTxFeePercent: 0,
|
||||
// SystemProtocol: 'ETHERC20',
|
||||
// IsActive: true,
|
||||
// InfoMessage: '',
|
||||
// MaintenanceMode: false,
|
||||
// MaintenanceMessage: '',
|
||||
// FormatPrefix: '',
|
||||
// FormatSufix: '',
|
||||
// DecimalSeparator: '.',
|
||||
// ThousandSeparator: ',',
|
||||
// DecimalPlaces: 8,
|
||||
// Currency: 'USDT',
|
||||
// CurrencyLong: 'Tether',
|
||||
// CoinType: 'ETHERC20' }
|
||||
const item = items[i];
|
||||
const id = this.safeString (item, 'Asset');
|
||||
const code = this.safeCurrencyCode (id);
|
||||
result[code] = {
|
||||
'id': id,
|
||||
'code': code,
|
||||
'name': this.safeString (item, 'AssetLong'),
|
||||
'active': this.safeValue (item, 'IsActive') && !this.safeValue (item, 'MaintenanceMode'),
|
||||
'fee': this.safeFloat (item, 'WithdrawTxFee'),
|
||||
'precision': this.safeFloat (item, 'DecimalPlaces'),
|
||||
'info': item,
|
||||
'limits': this.limits,
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async fetchMarkets (params = {}) {
|
||||
// https://github.com/ccxt/ccxt/issues/5668
|
||||
const response = await this.v3PublicGetGetmarkets (params);
|
||||
const result = [];
|
||||
const markets = this.safeValue (response, 'result');
|
||||
for (let i = 0; i < markets.length; i++) {
|
||||
const market = markets[i];
|
||||
// { MarketName: 'LTC_USDT',
|
||||
// MarketAsset: 'LTC',
|
||||
// BaseAsset: 'USDT',
|
||||
// MarketAssetLong: 'Litecoin',
|
||||
// BaseAssetLong: 'Tether',
|
||||
// IsActive: true,
|
||||
// MinTradeSize: 0.0001,
|
||||
// InfoMessage: '',
|
||||
// MarketCurrency: 'LTC',
|
||||
// BaseCurrency: 'USDT',
|
||||
// MarketCurrencyLong: 'Litecoin',
|
||||
// BaseCurrencyLong: 'Tether' }
|
||||
const id = this.safeString (market, 'MarketName');
|
||||
const baseId = this.safeString (market, 'MarketAsset');
|
||||
const quoteId = this.safeString (market, 'BaseAsset');
|
||||
const base = this.safeCurrencyCode (baseId);
|
||||
const quote = this.safeCurrencyCode (quoteId);
|
||||
const symbol = base + '/' + quote;
|
||||
const precision = {
|
||||
'amount': 8,
|
||||
'price': 8,
|
||||
};
|
||||
const active = this.safeValue (market, 'IsActive', false);
|
||||
result.push ({
|
||||
'id': id,
|
||||
'symbol': symbol,
|
||||
'base': base,
|
||||
'quote': quote,
|
||||
'baseId': baseId,
|
||||
'quoteId': quoteId,
|
||||
'active': active,
|
||||
'info': market,
|
||||
'precision': precision,
|
||||
'maker': this.fees['trading']['maker'],
|
||||
'taker': this.fees['trading']['taker'],
|
||||
'limits': {
|
||||
'amount': {
|
||||
'min': this.safeFloat (market, 'MinTradeSize'),
|
||||
'max': undefined,
|
||||
},
|
||||
'price': {
|
||||
'min': Math.pow (10, -precision['price']),
|
||||
'max': undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async fetchOrderBook (symbol, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const request = {
|
||||
'market': this.marketId (symbol),
|
||||
'type': 'ALL',
|
||||
};
|
||||
if (limit !== undefined) {
|
||||
request['depth'] = limit; // 50
|
||||
}
|
||||
const response = await this.v3PublicGetGetorderbook (this.extend (request, params));
|
||||
const orderbook = this.safeValue (response, 'result');
|
||||
if (!orderbook) {
|
||||
throw new ExchangeError (this.id + ' no orderbook data in ' + this.json (response));
|
||||
}
|
||||
return this.parseOrderBook (orderbook, undefined, 'buy', 'sell', 'Rate', 'Quantity');
|
||||
}
|
||||
|
||||
async fetchTicker (symbol, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const request = {
|
||||
'market': market['id'],
|
||||
};
|
||||
const response = await this.v3PublicGetGetmarketsummary (this.extend (request, params));
|
||||
const ticker = response['result'][0];
|
||||
return this.parseTicker (ticker, market);
|
||||
}
|
||||
|
||||
async fetchTickers (symbols = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const response = await this.v3PublicGetGetmarketsummaries (params);
|
||||
const result = this.safeValue (response, 'result');
|
||||
const tickers = [];
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
const ticker = this.parseTicker (result[i]);
|
||||
tickers.push (ticker);
|
||||
}
|
||||
return this.filterByArray (tickers, 'symbol', symbols);
|
||||
}
|
||||
|
||||
parseTicker (ticker, market = undefined) {
|
||||
// { TimeStamp: '2020-01-14 14:32:28',
|
||||
// MarketName: 'LTC_USDT',
|
||||
// MarketAsset: 'LTC',
|
||||
// BaseAsset: 'USDT',
|
||||
// MarketAssetName: 'Litecoin',
|
||||
// BaseAssetName: 'Tether',
|
||||
// PrevDay: 49.2867503,
|
||||
// High: 56.78622664,
|
||||
// Low: 49.27384025,
|
||||
// Last: 53.94,
|
||||
// Average: 51.37509368,
|
||||
// Volume: 1.51282404,
|
||||
// BaseVolume: 77.72147677,
|
||||
// Bid: 53.62070218,
|
||||
// Ask: 53.94,
|
||||
// IsActive: 'true',
|
||||
// InfoMessage: '',
|
||||
// MarketCurrency: 'Litecoin',
|
||||
// BaseCurrency: 'Tether' }
|
||||
const timestamp = this.parse8601 (this.safeString (ticker, 'TimeStamp'));
|
||||
let symbol = undefined;
|
||||
const marketId = this.safeString (ticker, 'MarketName');
|
||||
if (marketId !== undefined) {
|
||||
if (marketId in this.markets_by_id) {
|
||||
market = this.markets_by_id[marketId];
|
||||
} else {
|
||||
symbol = this.parseSymbol (marketId);
|
||||
}
|
||||
}
|
||||
if ((symbol === undefined) && (market !== undefined)) {
|
||||
symbol = market['symbol'];
|
||||
}
|
||||
const previous = this.safeFloat (ticker, 'PrevDay');
|
||||
const last = this.safeFloat (ticker, 'Last');
|
||||
let change = undefined;
|
||||
let percentage = undefined;
|
||||
if (last !== undefined) {
|
||||
if (previous !== undefined) {
|
||||
change = last - previous;
|
||||
if (previous > 0) {
|
||||
percentage = (change / previous) * 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
'symbol': symbol,
|
||||
'timestamp': timestamp,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'high': this.safeFloat (ticker, 'High'),
|
||||
'low': this.safeFloat (ticker, 'Low'),
|
||||
'bid': this.safeFloat (ticker, 'Bid'),
|
||||
'bidVolume': undefined,
|
||||
'ask': this.safeFloat (ticker, 'Ask'),
|
||||
'askVolume': undefined,
|
||||
'vwap': undefined,
|
||||
'open': previous,
|
||||
'close': last,
|
||||
'last': last,
|
||||
'previousClose': undefined,
|
||||
'change': change,
|
||||
'percentage': percentage,
|
||||
'average': undefined,
|
||||
'baseVolume': this.safeFloat (ticker, 'Volume'),
|
||||
'quoteVolume': this.safeFloat (ticker, 'BaseVolume'),
|
||||
'info': ticker,
|
||||
};
|
||||
}
|
||||
|
||||
parseOHLCV (ohlcv, market = undefined, timeframe = '1d', since = undefined, limit = undefined) {
|
||||
const timestamp = this.parse8601 (ohlcv['TimeStamp'] + '+00:00');
|
||||
return [
|
||||
timestamp,
|
||||
this.safeFloat (ohlcv, 'Open'),
|
||||
this.safeFloat (ohlcv, 'High'),
|
||||
this.safeFloat (ohlcv, 'Low'),
|
||||
this.safeFloat (ohlcv, 'Close'),
|
||||
this.safeFloat (ohlcv, 'Volume'),
|
||||
];
|
||||
}
|
||||
|
||||
async fetchOHLCV (symbol, timeframe = '15m', since = undefined, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const request = {
|
||||
'period': this.timeframes[timeframe],
|
||||
'market': market['id'],
|
||||
'count': limit,
|
||||
};
|
||||
const response = await this.v3PublicGetGetcandles (this.extend (request, params));
|
||||
return this.parseOHLCVs (response['result'], market, timeframe, since, limit);
|
||||
}
|
||||
|
||||
async createOrder (symbol, type, side, amount, price = undefined, params = {}) {
|
||||
if (type !== 'limit') {
|
||||
// todo: STOP-LIMIT and AMI order types are supported
|
||||
throw new InvalidOrder (this.id + ' allows limit orders only');
|
||||
}
|
||||
await this.loadMarkets ();
|
||||
const request = {
|
||||
'rate': this.priceToPrecision (symbol, price),
|
||||
'quantity': this.amountToPrecision (symbol, amount),
|
||||
'tradeType': (side === 'buy') ? '1' : '0',
|
||||
'market': this.marketId (symbol),
|
||||
};
|
||||
let response = undefined;
|
||||
if (side === 'buy') {
|
||||
response = await this.v3PrivatePostBuylimit (this.extend (request, params));
|
||||
} else {
|
||||
response = await this.v3PrivatePostSelllimit (this.extend (request, params));
|
||||
}
|
||||
// { success: true,
|
||||
// message: "",
|
||||
// result: "161105236" },
|
||||
return {
|
||||
'info': response,
|
||||
'id': this.safeString (response, 'result'),
|
||||
};
|
||||
}
|
||||
|
||||
async cancelOrder (id, symbol = undefined, params = {}) {
|
||||
const request = {
|
||||
'orderid': id,
|
||||
};
|
||||
const response = await this.v3PrivatePostOrdercancel (this.extend (request, params));
|
||||
// { success: true, message: '', result: '' }
|
||||
return response;
|
||||
}
|
||||
|
||||
async fetchOpenOrders (symbol = undefined, since = undefined, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
let market = undefined;
|
||||
const request = {};
|
||||
if (symbol !== undefined) {
|
||||
market = this.market (symbol);
|
||||
request['market'] = market['id'];
|
||||
}
|
||||
const response = await this.v3PrivatePostGetopenorders (this.extend (request, params));
|
||||
const items = this.safeValue (response, 'result', []);
|
||||
return this.parseOrders (items, market, since, limit);
|
||||
}
|
||||
|
||||
parseSymbol (id) {
|
||||
let [ base, quote ] = id.split (this.options['symbolSeparator']);
|
||||
base = this.safeCurrencyCode (base);
|
||||
quote = this.safeCurrencyCode (quote);
|
||||
return base + '/' + quote;
|
||||
}
|
||||
|
||||
async fetchBalance (params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const response = await this.v3PrivatePostGetbalances (params);
|
||||
const result = { 'info': response };
|
||||
const items = response['result'];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
const currencyId = this.safeString (item, 'Asset');
|
||||
const code = this.safeCurrencyCode (currencyId);
|
||||
const account = this.account ();
|
||||
account['free'] = this.safeFloat (item, 'Available');
|
||||
account['total'] = this.safeFloat (item, 'Balance');
|
||||
result[code] = account;
|
||||
}
|
||||
return this.parseBalance (result);
|
||||
}
|
||||
|
||||
async fetchClosedOrders (symbol = undefined, since = undefined, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const request = {};
|
||||
let market = undefined;
|
||||
if (symbol !== undefined) {
|
||||
market = this.market (symbol);
|
||||
request['market'] = market['id'];
|
||||
}
|
||||
const response = await this.v3PrivatePostGetcloseorders (this.extend (request, params));
|
||||
const orders = this.safeValue (response, 'result', []);
|
||||
return this.parseOrders (orders, market, since, limit);
|
||||
}
|
||||
|
||||
async fetchTransactionsWithMethod (method, code = undefined, since = undefined, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const response = await this[method] (params);
|
||||
const transactions = this.safeValue (response, 'result', []);
|
||||
return this.parseTransactions (transactions, code, since, limit);
|
||||
}
|
||||
|
||||
async fetchDeposits (code = undefined, since = undefined, limit = undefined, params = {}) {
|
||||
return await this.fetchTransactionsWithMethod ('v3PrivatePostGetdeposithistory', code, since, limit, params);
|
||||
}
|
||||
|
||||
async fetchWithdrawals (code = undefined, since = undefined, limit = undefined, params = {}) {
|
||||
return await this.fetchTransactionsWithMethod ('v3PrivatePostGetwithdrawhistory', code, since, limit, params);
|
||||
}
|
||||
|
||||
async fetchDepositAddress (code, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const currency = this.currency (code);
|
||||
const request = {
|
||||
'asset': currency['id'],
|
||||
};
|
||||
const response = await this.v3PrivatePostGetdepositaddress (this.extend (request, params));
|
||||
// { success: true,
|
||||
// message: '',
|
||||
// result:
|
||||
// { Asset: 'ETH',
|
||||
// AssetName: 'Ethereum',
|
||||
// DepositAddress: '0x748c5c8jhksjdfhd507d3aa9',
|
||||
// Currency: 'ETH',
|
||||
// CurrencyName: 'Ethereum' } }
|
||||
const item = response['result'];
|
||||
const address = this.safeString (item, 'DepositAddress');
|
||||
return {
|
||||
'currency': code,
|
||||
'address': this.checkAddress (address),
|
||||
// 'tag': tag,
|
||||
'info': item,
|
||||
};
|
||||
}
|
||||
|
||||
parseLedgerEntryType (type) {
|
||||
// deposits don't seem to appear in here
|
||||
const types = {
|
||||
'TRADE': 'trade',
|
||||
'WITHDRAW': 'transaction',
|
||||
};
|
||||
return this.safeString (types, type, type);
|
||||
}
|
||||
|
||||
parseLedgerEntry (item, currency = undefined) {
|
||||
//
|
||||
// trade (both sides)
|
||||
//
|
||||
// {
|
||||
// ID: 109660527,
|
||||
// TimeStamp: '2018-11-14 15:12:57.140776',
|
||||
// Asset: 'ETH',
|
||||
// AssetName: 'Ethereum',
|
||||
// Amount: 0.01,
|
||||
// Type: 'TRADE',
|
||||
// Description: 'Trade +, order id 133111123',
|
||||
// Comments: '',
|
||||
// CoinSymbol: 'ETH',
|
||||
// CoinName: 'Ethereum'
|
||||
// }
|
||||
//
|
||||
// {
|
||||
// ID: 109660526,
|
||||
// TimeStamp: '2018-11-14 15:12:57.140776',
|
||||
// Asset: 'BTC',
|
||||
// AssetName: 'Bitcoin',
|
||||
// Amount: -0.00031776,
|
||||
// Type: 'TRADE',
|
||||
// Description: 'Trade -, order id 133111123, fee -0.00000079',
|
||||
// Comments: '',
|
||||
// CoinSymbol: 'BTC',
|
||||
// CoinName: 'Bitcoin'
|
||||
// }
|
||||
//
|
||||
// withdrawal
|
||||
//
|
||||
// {
|
||||
// ID: 104672316,
|
||||
// TimeStamp: '2018-05-03 08:18:19.031831',
|
||||
// Asset: 'DOGE',
|
||||
// AssetName: 'Dogecoin',
|
||||
// Amount: -61893.87864686,
|
||||
// Type: 'WITHDRAW',
|
||||
// Description: 'Withdraw: 61883.87864686 to address DD8tgehNNyYB2iqVazi2W1paaztgcWXtF6; fee 10.00000000',
|
||||
// Comments: '',
|
||||
// CoinSymbol: 'DOGE',
|
||||
// CoinName: 'Dogecoin'
|
||||
// }
|
||||
//
|
||||
const code = this.safeCurrencyCode (this.safeString (item, 'CoinSymbol'), currency);
|
||||
const description = this.safeString (item, 'Description');
|
||||
const type = this.parseLedgerEntryType (this.safeString (item, 'Type'));
|
||||
let referenceId = undefined;
|
||||
let fee = undefined;
|
||||
const delimiter = (type === 'trade') ? ', ' : '; ';
|
||||
const parts = description.split (delimiter);
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
let part = parts[i];
|
||||
if (part.indexOf ('fee') === 0) {
|
||||
part = part.replace ('fee ', '');
|
||||
let feeCost = parseFloat (part);
|
||||
if (feeCost < 0) {
|
||||
feeCost = -feeCost;
|
||||
}
|
||||
fee = {
|
||||
'cost': feeCost,
|
||||
'currency': code,
|
||||
};
|
||||
} else if (part.indexOf ('order id') === 0) {
|
||||
referenceId = part.replace ('order id ', '');
|
||||
}
|
||||
//
|
||||
// does not belong to Ledger, related to parseTransaction
|
||||
//
|
||||
// if (part.indexOf ('Withdraw') === 0) {
|
||||
// const details = part.split (' to address ');
|
||||
// if (details.length > 1) {
|
||||
// address = details[1];
|
||||
// }
|
||||
//
|
||||
}
|
||||
const timestamp = this.parse8601 (this.safeString (item, 'TimeStamp'));
|
||||
let amount = this.safeFloat (item, 'Amount');
|
||||
let direction = undefined;
|
||||
if (amount !== undefined) {
|
||||
direction = 'in';
|
||||
if (amount < 0) {
|
||||
direction = 'out';
|
||||
amount = -amount;
|
||||
}
|
||||
}
|
||||
const id = this.safeString (item, 'ID');
|
||||
return {
|
||||
'id': id,
|
||||
'info': item,
|
||||
'timestamp': timestamp,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'direction': direction,
|
||||
'account': undefined,
|
||||
'referenceId': referenceId,
|
||||
'referenceAccount': undefined,
|
||||
'type': type,
|
||||
'currency': code,
|
||||
'amount': amount,
|
||||
'before': undefined,
|
||||
'after': undefined,
|
||||
'status': 'ok',
|
||||
'fee': fee,
|
||||
};
|
||||
}
|
||||
|
||||
async fetchLedger (code = undefined, since = undefined, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
// only seems to return 100 items and there is no documented way to change page size or offset
|
||||
const request = {
|
||||
};
|
||||
const response = await this.v3PrivatePostGetmytransactions (this.extend (request, params));
|
||||
const items = response['result'];
|
||||
return this.parseLedger (items, code, since, limit);
|
||||
}
|
||||
|
||||
parseOrder (order, market = undefined) {
|
||||
//
|
||||
// fetchClosedOrders
|
||||
//
|
||||
// { OrderID: 89742658,
|
||||
// Exchange: 'DOGE_BTC',
|
||||
// Type: 'BUY',
|
||||
// Quantity: 10000,
|
||||
// QuantityRemaining: 0,
|
||||
// QuantityBaseTraded: 0,
|
||||
// Price: 6.6e-7,
|
||||
// Status: 'OK',
|
||||
// Created: '2018-02-16 08:55:36',
|
||||
// Comments: '' }
|
||||
//
|
||||
// fetchOpenOrders
|
||||
//
|
||||
// { OrderID: 161105302,
|
||||
// Exchange: 'ETH_BTC',
|
||||
// Type: 'SELL',
|
||||
// Quantity: 0.4,
|
||||
// QuantityRemaining: 0.4,
|
||||
// QuantityBaseTraded: 0,
|
||||
// Price: 0.04,
|
||||
// Status: 'OPEN',
|
||||
// Created: '2020-01-22 09:21:27',
|
||||
// Comments: { String: '', Valid: true }
|
||||
const side = this.safeString (order, 'Type').toLowerCase ();
|
||||
const status = this.parseOrderStatus (this.safeString (order, 'Status'));
|
||||
let symbol = undefined;
|
||||
const marketId = this.safeString (order, 'Exchange');
|
||||
if (marketId === undefined) {
|
||||
if (market !== undefined) {
|
||||
symbol = market['symbol'];
|
||||
}
|
||||
} else {
|
||||
if (marketId in this.markets_by_id) {
|
||||
market = this.markets_by_id[marketId];
|
||||
symbol = market['symbol'];
|
||||
} else {
|
||||
symbol = this.parseSymbol (marketId);
|
||||
}
|
||||
}
|
||||
let timestamp = undefined;
|
||||
if ('Created' in order) {
|
||||
timestamp = this.parse8601 (order['Created'] + '+00:00');
|
||||
}
|
||||
let price = this.safeFloat (order, 'Price');
|
||||
let cost = undefined;
|
||||
const amount = this.safeFloat (order, 'Quantity');
|
||||
const remaining = this.safeFloat (order, 'QuantityRemaining');
|
||||
let filled = undefined;
|
||||
if (amount !== undefined && remaining !== undefined) {
|
||||
filled = amount - remaining;
|
||||
}
|
||||
if (!cost) {
|
||||
if (price && filled) {
|
||||
cost = price * filled;
|
||||
}
|
||||
}
|
||||
if (!price) {
|
||||
if (cost && filled) {
|
||||
price = cost / filled;
|
||||
}
|
||||
}
|
||||
const average = this.safeFloat (order, 'PricePerUnit');
|
||||
const id = this.safeString (order, 'OrderID');
|
||||
return {
|
||||
'info': order,
|
||||
'id': id,
|
||||
'clientOrderId': undefined,
|
||||
'timestamp': timestamp,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'lastTradeTimestamp': undefined,
|
||||
'symbol': symbol,
|
||||
'type': 'limit',
|
||||
'side': side,
|
||||
'price': price,
|
||||
'cost': cost,
|
||||
'average': average,
|
||||
'amount': amount,
|
||||
'filled': filled,
|
||||
'remaining': remaining,
|
||||
'status': status,
|
||||
'fee': undefined,
|
||||
'trades': undefined,
|
||||
};
|
||||
}
|
||||
|
||||
parseOrderStatus (status) {
|
||||
const statuses = {
|
||||
'OK': 'closed',
|
||||
'OPEN': 'open',
|
||||
'CANCELED': 'canceled',
|
||||
};
|
||||
return this.safeString (statuses, status, status);
|
||||
}
|
||||
|
||||
parseTransaction (transaction, currency = undefined) {
|
||||
//
|
||||
// deposit:
|
||||
//
|
||||
// { ID: 118698752,
|
||||
// Timestamp: '2020-01-21 11:16:09',
|
||||
// Asset: 'ETH',
|
||||
// Amount: 1,
|
||||
// TransactionID: '',
|
||||
// Status: 'CONFIRMED',
|
||||
// Label: '0x748c5c8228d0c596f4d07f338blah',
|
||||
// Symbol: 'ETH' }
|
||||
//
|
||||
// withdrawal:
|
||||
//
|
||||
// { ID: 689281,
|
||||
// Timestamp: '2019-07-05 13:14:43',
|
||||
// Asset: 'BTC',
|
||||
// Amount: -0.108959,
|
||||
// TransactionID: 'da48d6901fslfjsdjflsdjfls852b87e362cad1',
|
||||
// Status: 'CONFIRMED',
|
||||
// Label: '0.1089590;35wztHPMgrebFvvblah;0.00100000',
|
||||
// Symbol: 'BTC' }
|
||||
//
|
||||
const id = this.safeString (transaction, 'ID');
|
||||
let amount = this.safeFloat (transaction, 'Amount');
|
||||
let type = 'deposit';
|
||||
if (amount < 0) {
|
||||
amount = Math.abs (amount);
|
||||
type = 'withdrawal';
|
||||
}
|
||||
const currencyId = this.safeString (transaction, 'Asset');
|
||||
const code = this.safeCurrencyCode (currencyId, currency);
|
||||
const label = this.safeString (transaction, 'Label');
|
||||
const timestamp = this.parse8601 (this.safeString (transaction, 'Timestamp'));
|
||||
let txid = this.safeString (transaction, 'TransactionID');
|
||||
let address = undefined;
|
||||
let feeCost = undefined;
|
||||
const labelParts = label.split (';');
|
||||
if (labelParts.length === 3) {
|
||||
amount = parseFloat (labelParts[0]);
|
||||
address = labelParts[1];
|
||||
feeCost = parseFloat (labelParts[2]);
|
||||
} else {
|
||||
address = label;
|
||||
}
|
||||
let fee = undefined;
|
||||
if (feeCost !== undefined) {
|
||||
fee = {
|
||||
'currency': code,
|
||||
'cost': feeCost,
|
||||
};
|
||||
}
|
||||
let status = 'ok';
|
||||
if (txid === 'CANCELED') {
|
||||
txid = undefined;
|
||||
status = 'canceled';
|
||||
}
|
||||
return {
|
||||
'info': transaction,
|
||||
'timestamp': timestamp,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'id': id,
|
||||
'currency': code,
|
||||
'amount': amount,
|
||||
'address': address,
|
||||
'tag': undefined,
|
||||
'status': status,
|
||||
'type': type,
|
||||
'updated': undefined,
|
||||
'txid': txid,
|
||||
'fee': fee,
|
||||
};
|
||||
}
|
||||
|
||||
nonce () {
|
||||
return this.milliseconds ();
|
||||
}
|
||||
|
||||
sign (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) {
|
||||
let url = this.implodeParams (this.urls['api'][api], {
|
||||
'hostname': this.hostname,
|
||||
}) + '/';
|
||||
if (api === 'v3Private') {
|
||||
this.checkRequiredCredentials ();
|
||||
const request = {
|
||||
'apikey': this.apiKey,
|
||||
'nonce': this.nonce (),
|
||||
};
|
||||
url += path + '?' + this.urlencode (this.extend (request, params));
|
||||
const signature = this.hmac (this.encode (url), this.encode (this.secret), 'sha512');
|
||||
headers = { 'apisign': signature };
|
||||
} else {
|
||||
url += path + '?' + this.urlencode (params);
|
||||
}
|
||||
return { 'url': url, 'method': method, 'body': body, 'headers': headers };
|
||||
}
|
||||
|
||||
handleErrors (code, reason, url, method, headers, body, response, requestHeaders, requestBody) {
|
||||
if (response === undefined) {
|
||||
return; // fallback to default error handler
|
||||
}
|
||||
// examples...
|
||||
// {"success":false,"message":"Erro: Order is not open.","result":""} <-- 'error' is spelt wrong
|
||||
// {"success":false,"message":"Error: Very low volume.","result":"ERR_LOW_VOLUME"}
|
||||
// {"success":false,"message":"Error: Insuficient Balance","result":"ERR_INSUFICIENT_BALANCE"}
|
||||
//
|
||||
if (body[0] === '{') {
|
||||
const success = this.safeValue (response, 'success');
|
||||
if (success === undefined) {
|
||||
throw new ExchangeError (this.id + ': malformed response: ' + this.json (response));
|
||||
}
|
||||
if (!success) {
|
||||
const feedback = this.id + ' ' + body;
|
||||
const errorCode = this.safeString (response, 'result');
|
||||
this.throwBroadlyMatchedException (this.exceptions['broad'], errorCode, feedback);
|
||||
this.throwExactlyMatchedException (this.exceptions['exact'], errorCode, feedback);
|
||||
const errorMessage = this.safeString (response, 'message');
|
||||
this.throwBroadlyMatchedException (this.exceptions['broad'], errorMessage, feedback);
|
||||
this.throwExactlyMatchedException (this.exceptions['exact'], errorMessage, feedback);
|
||||
throw new ExchangeError (feedback);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
+618
@@ -0,0 +1,618 @@
|
||||
'use strict';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const Exchange = require ('./base/Exchange');
|
||||
const { ExchangeError, InvalidOrder, AuthenticationError, ArgumentsRequired } = require ('./base/errors');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
module.exports = class braziliex extends Exchange {
|
||||
describe () {
|
||||
return this.deepExtend (super.describe (), {
|
||||
'id': 'braziliex',
|
||||
'name': 'Braziliex',
|
||||
'countries': [ 'BR' ],
|
||||
'rateLimit': 1000,
|
||||
'has': {
|
||||
'fetchCurrencies': true,
|
||||
'fetchTickers': true,
|
||||
'fetchOpenOrders': true,
|
||||
'fetchMyTrades': true,
|
||||
'fetchDepositAddress': true,
|
||||
'fetchOrder': true,
|
||||
},
|
||||
'urls': {
|
||||
'logo': 'https://user-images.githubusercontent.com/1294454/34703593-c4498674-f504-11e7-8d14-ff8e44fb78c1.jpg',
|
||||
'api': 'https://braziliex.com/api/v1',
|
||||
'www': 'https://braziliex.com/',
|
||||
'doc': 'https://braziliex.com/exchange/api.php',
|
||||
'fees': 'https://braziliex.com/exchange/fees.php',
|
||||
'referral': 'https://braziliex.com/?ref=5FE61AB6F6D67DA885BC98BA27223465',
|
||||
},
|
||||
'api': {
|
||||
'public': {
|
||||
'get': [
|
||||
'currencies',
|
||||
'ticker',
|
||||
'ticker/{market}',
|
||||
'orderbook/{market}',
|
||||
'tradehistory/{market}',
|
||||
],
|
||||
},
|
||||
'private': {
|
||||
'post': [
|
||||
'balance',
|
||||
'complete_balance',
|
||||
'open_orders',
|
||||
'trade_history',
|
||||
'deposit_address',
|
||||
'sell',
|
||||
'buy',
|
||||
'cancel_order',
|
||||
'order_status',
|
||||
],
|
||||
},
|
||||
},
|
||||
'commonCurrencies': {
|
||||
'EPC': 'Epacoin',
|
||||
'ABC': 'Anti Bureaucracy Coin',
|
||||
},
|
||||
'fees': {
|
||||
'trading': {
|
||||
'maker': 0.005,
|
||||
'taker': 0.005,
|
||||
},
|
||||
},
|
||||
'precision': {
|
||||
'amount': 8,
|
||||
'price': 8,
|
||||
},
|
||||
'options': {
|
||||
'fetchCurrencies': {
|
||||
'expires': 1000, // 1 second
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async fetchCurrenciesFromCache (params = {}) {
|
||||
// this method is now redundant
|
||||
// currencies are now fetched before markets
|
||||
const options = this.safeValue (this.options, 'fetchCurrencies', {});
|
||||
const timestamp = this.safeInteger (options, 'timestamp');
|
||||
const expires = this.safeInteger (options, 'expires', 1000);
|
||||
const now = this.milliseconds ();
|
||||
if ((timestamp === undefined) || ((now - timestamp) > expires)) {
|
||||
const response = await this.publicGetCurrencies (params);
|
||||
this.options['fetchCurrencies'] = this.extend (options, {
|
||||
'response': response,
|
||||
'timestamp': now,
|
||||
});
|
||||
}
|
||||
return this.safeValue (this.options['fetchCurrencies'], 'response');
|
||||
}
|
||||
|
||||
async fetchCurrencies (params = {}) {
|
||||
const response = await this.fetchCurrenciesFromCache (params);
|
||||
//
|
||||
// {
|
||||
// brl: {
|
||||
// name: "Real",
|
||||
// withdrawal_txFee: 0.0075,
|
||||
// txWithdrawalFee: 9,
|
||||
// MinWithdrawal: 30,
|
||||
// minConf: 1,
|
||||
// minDeposit: 0,
|
||||
// txDepositFee: 0,
|
||||
// txDepositPercentageFee: 0,
|
||||
// minAmountTradeFIAT: 5,
|
||||
// minAmountTradeBTC: 0.0001,
|
||||
// minAmountTradeUSDT: 0.0001,
|
||||
// decimal: 8,
|
||||
// decimal_withdrawal: 8,
|
||||
// active: 1,
|
||||
// dev_active: 1,
|
||||
// under_maintenance: 0,
|
||||
// order: "010",
|
||||
// is_withdrawal_active: 1,
|
||||
// is_deposit_active: 1,
|
||||
// is_token_erc20: 0,
|
||||
// is_fiat: 1,
|
||||
// gateway: 0,
|
||||
// },
|
||||
// btc: {
|
||||
// name: "Bitcoin",
|
||||
// txWithdrawalMinFee: 0.000125,
|
||||
// txWithdrawalFee: 0.00015625,
|
||||
// MinWithdrawal: 0.0005,
|
||||
// minConf: 1,
|
||||
// minDeposit: 0,
|
||||
// txDepositFee: 0,
|
||||
// txDepositPercentageFee: 0,
|
||||
// minAmountTradeFIAT: 5,
|
||||
// minAmountTradeBTC: 0.0001,
|
||||
// minAmountTradeUSDT: 0.0001,
|
||||
// decimal: 8,
|
||||
// decimal_withdrawal: 8,
|
||||
// active: 1,
|
||||
// dev_active: 1,
|
||||
// under_maintenance: 0,
|
||||
// order: "011",
|
||||
// is_withdrawal_active: 1,
|
||||
// is_deposit_active: 1,
|
||||
// is_token_erc20: 0,
|
||||
// is_fiat: 0,
|
||||
// gateway: 1,
|
||||
// }
|
||||
// }
|
||||
//
|
||||
this.options['currencies'] = {
|
||||
'timestamp': this.milliseconds (),
|
||||
'response': response,
|
||||
};
|
||||
const ids = Object.keys (response);
|
||||
const result = {};
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
const id = ids[i];
|
||||
const currency = response[id];
|
||||
const precision = this.safeInteger (currency, 'decimal');
|
||||
const code = this.safeCurrencyCode (id);
|
||||
let active = this.safeInteger (currency, 'active') === 1;
|
||||
const maintenance = this.safeInteger (currency, 'under_maintenance');
|
||||
if (maintenance !== 0) {
|
||||
active = false;
|
||||
}
|
||||
const canWithdraw = this.safeInteger (currency, 'is_withdrawal_active') === 1;
|
||||
const canDeposit = this.safeInteger (currency, 'is_deposit_active') === 1;
|
||||
if (!canWithdraw || !canDeposit) {
|
||||
active = false;
|
||||
}
|
||||
result[code] = {
|
||||
'id': id,
|
||||
'code': code,
|
||||
'name': currency['name'],
|
||||
'active': active,
|
||||
'precision': precision,
|
||||
'funding': {
|
||||
'withdraw': {
|
||||
'active': canWithdraw,
|
||||
'fee': this.safeFloat (currency, 'txWithdrawalFee'),
|
||||
},
|
||||
'deposit': {
|
||||
'active': canDeposit,
|
||||
'fee': this.safeFloat (currency, 'txDepositFee'),
|
||||
},
|
||||
},
|
||||
'limits': {
|
||||
'amount': {
|
||||
'min': Math.pow (10, -precision),
|
||||
'max': Math.pow (10, precision),
|
||||
},
|
||||
'price': {
|
||||
'min': Math.pow (10, -precision),
|
||||
'max': Math.pow (10, precision),
|
||||
},
|
||||
'cost': {
|
||||
'min': undefined,
|
||||
'max': undefined,
|
||||
},
|
||||
'withdraw': {
|
||||
'min': this.safeFloat (currency, 'MinWithdrawal'),
|
||||
'max': Math.pow (10, precision),
|
||||
},
|
||||
'deposit': {
|
||||
'min': this.safeFloat (currency, 'minDeposit'),
|
||||
'max': undefined,
|
||||
},
|
||||
},
|
||||
'info': currency,
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async fetchMarkets (params = {}) {
|
||||
const currencies = await this.fetchCurrenciesFromCache (params);
|
||||
const response = await this.publicGetTicker ();
|
||||
//
|
||||
// {
|
||||
// btc_brl: {
|
||||
// active: 1,
|
||||
// market: 'btc_brl',
|
||||
// last: 14648,
|
||||
// percentChange: -0.95,
|
||||
// baseVolume24: 27.856,
|
||||
// quoteVolume24: 409328.039,
|
||||
// baseVolume: 27.856,
|
||||
// quoteVolume: 409328.039,
|
||||
// highestBid24: 14790,
|
||||
// lowestAsk24: 14450.01,
|
||||
// highestBid: 14450.37,
|
||||
// lowestAsk: 14699.98
|
||||
// },
|
||||
// ...
|
||||
// }
|
||||
//
|
||||
const ids = Object.keys (response);
|
||||
const result = [];
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
const id = ids[i];
|
||||
const market = response[id];
|
||||
const [ baseId, quoteId ] = id.split ('_');
|
||||
const uppercaseBaseId = baseId.toUpperCase ();
|
||||
const uppercaseQuoteId = quoteId.toUpperCase ();
|
||||
const base = this.safeCurrencyCode (uppercaseBaseId);
|
||||
const quote = this.safeCurrencyCode (uppercaseQuoteId);
|
||||
const symbol = base + '/' + quote;
|
||||
const baseCurrency = this.safeValue (currencies, baseId, {});
|
||||
const quoteCurrency = this.safeValue (currencies, quoteId, {});
|
||||
const quoteIsFiat = this.safeInteger (quoteCurrency, 'is_fiat', 0);
|
||||
let minCost = undefined;
|
||||
if (quoteIsFiat) {
|
||||
minCost = this.safeFloat (baseCurrency, 'minAmountTradeFIAT');
|
||||
} else {
|
||||
minCost = this.safeFloat (baseCurrency, 'minAmountTrade' + uppercaseQuoteId);
|
||||
}
|
||||
const isActive = this.safeInteger (market, 'active');
|
||||
const active = (isActive === 1);
|
||||
const precision = {
|
||||
'amount': 8,
|
||||
'price': 8,
|
||||
};
|
||||
result.push ({
|
||||
'id': id,
|
||||
'symbol': symbol,
|
||||
'base': base,
|
||||
'quote': quote,
|
||||
'baseId': baseId,
|
||||
'quoteId': quoteId,
|
||||
'active': active,
|
||||
'precision': precision,
|
||||
'limits': {
|
||||
'amount': {
|
||||
'min': Math.pow (10, -precision['amount']),
|
||||
'max': Math.pow (10, precision['amount']),
|
||||
},
|
||||
'price': {
|
||||
'min': Math.pow (10, -precision['price']),
|
||||
'max': Math.pow (10, precision['price']),
|
||||
},
|
||||
'cost': {
|
||||
'min': minCost,
|
||||
'max': undefined,
|
||||
},
|
||||
},
|
||||
'info': market,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
parseTicker (ticker, market = undefined) {
|
||||
let symbol = undefined;
|
||||
if (market !== undefined) {
|
||||
symbol = market['symbol'];
|
||||
}
|
||||
const timestamp = this.milliseconds ();
|
||||
const last = this.safeFloat (ticker, 'last');
|
||||
return {
|
||||
'symbol': symbol,
|
||||
'timestamp': timestamp,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'high': this.safeFloat (ticker, 'highestBid24'),
|
||||
'low': this.safeFloat (ticker, 'lowestAsk24'),
|
||||
'bid': this.safeFloat (ticker, 'highestBid'),
|
||||
'bidVolume': undefined,
|
||||
'ask': this.safeFloat (ticker, 'lowestAsk'),
|
||||
'askVolume': undefined,
|
||||
'vwap': undefined,
|
||||
'open': undefined,
|
||||
'close': last,
|
||||
'last': last,
|
||||
'previousClose': undefined,
|
||||
'change': this.safeFloat (ticker, 'percentChange'),
|
||||
'percentage': undefined,
|
||||
'average': undefined,
|
||||
'baseVolume': this.safeFloat (ticker, 'baseVolume24'),
|
||||
'quoteVolume': this.safeFloat (ticker, 'quoteVolume24'),
|
||||
'info': ticker,
|
||||
};
|
||||
}
|
||||
|
||||
async fetchTicker (symbol, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const request = {
|
||||
'market': market['id'],
|
||||
};
|
||||
const response = await this.publicGetTickerMarket (this.extend (request, params));
|
||||
return this.parseTicker (response, market);
|
||||
}
|
||||
|
||||
async fetchTickers (symbols = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const response = await this.publicGetTicker (params);
|
||||
const result = {};
|
||||
const ids = Object.keys (response);
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
const marketId = ids[i];
|
||||
const market = this.markets_by_id[marketId];
|
||||
const symbol = market['symbol'];
|
||||
result[symbol] = this.parseTicker (response[marketId], market);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async fetchOrderBook (symbol, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const request = {
|
||||
'market': this.marketId (symbol),
|
||||
};
|
||||
const response = await this.publicGetOrderbookMarket (this.extend (request, params));
|
||||
return this.parseOrderBook (response, undefined, 'bids', 'asks', 'price', 'amount');
|
||||
}
|
||||
|
||||
parseTrade (trade, market = undefined) {
|
||||
const timestamp = this.parse8601 (this.safeString2 (trade, 'date_exec', 'date'));
|
||||
const price = this.safeFloat (trade, 'price');
|
||||
const amount = this.safeFloat (trade, 'amount');
|
||||
let symbol = undefined;
|
||||
if (market !== undefined) {
|
||||
symbol = market['symbol'];
|
||||
}
|
||||
const cost = this.safeFloat (trade, 'total');
|
||||
const orderId = this.safeString (trade, 'order_number');
|
||||
const type = 'limit';
|
||||
const side = this.safeString (trade, 'type');
|
||||
const id = this.safeString (trade, '_id');
|
||||
return {
|
||||
'id': id,
|
||||
'info': trade,
|
||||
'timestamp': timestamp,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'symbol': symbol,
|
||||
'order': orderId,
|
||||
'type': type,
|
||||
'side': side,
|
||||
'takerOrMaker': undefined,
|
||||
'price': price,
|
||||
'amount': amount,
|
||||
'cost': cost,
|
||||
'fee': undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async fetchTrades (symbol, since = undefined, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const request = {
|
||||
'market': market['id'],
|
||||
};
|
||||
const response = await this.publicGetTradehistoryMarket (this.extend (request, params));
|
||||
return this.parseTrades (response, market, since, limit);
|
||||
}
|
||||
|
||||
async fetchBalance (params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const balances = await this.privatePostCompleteBalance (params);
|
||||
const result = { 'info': balances };
|
||||
const currencyIds = Object.keys (balances);
|
||||
for (let i = 0; i < currencyIds.length; i++) {
|
||||
const currencyId = currencyIds[i];
|
||||
const balance = balances[currencyId];
|
||||
const code = this.safeCurrencyCode (currencyId);
|
||||
const account = this.account ();
|
||||
account['free'] = this.safeFloat (balance, 'available');
|
||||
account['total'] = this.safeFloat (balance, 'total');
|
||||
result[code] = account;
|
||||
}
|
||||
return this.parseBalance (result);
|
||||
}
|
||||
|
||||
parseOrder (order, market = undefined) {
|
||||
//
|
||||
// {
|
||||
// "order_number":"58ee441d05f8233fadabfb07",
|
||||
// "type":"buy",
|
||||
// "market":"ltc_btc",
|
||||
// "price":"0.01000000",
|
||||
// "amount":"0.00200000",
|
||||
// "total":"0.00002000",
|
||||
// "progress":"1.0000",
|
||||
// "date":"2017-03-12 15:13:33"
|
||||
// }
|
||||
//
|
||||
let symbol = undefined;
|
||||
if (market === undefined) {
|
||||
const marketId = this.safeString (order, 'market');
|
||||
if (marketId in this.markets_by_id) {
|
||||
market = this.markets_by_id[marketId];
|
||||
}
|
||||
}
|
||||
if (market !== undefined) {
|
||||
symbol = market['symbol'];
|
||||
}
|
||||
let timestamp = this.safeInteger (order, 'timestamp');
|
||||
if (timestamp === undefined) {
|
||||
timestamp = this.parse8601 (this.safeString (order, 'date'));
|
||||
}
|
||||
const price = this.safeFloat (order, 'price');
|
||||
const cost = this.safeFloat (order, 'total', 0.0);
|
||||
const amount = this.safeFloat (order, 'amount');
|
||||
const filledPercentage = this.safeFloat (order, 'progress');
|
||||
const filled = amount * filledPercentage;
|
||||
const remaining = parseFloat (this.amountToPrecision (symbol, amount - filled));
|
||||
let info = order;
|
||||
if ('info' in info) {
|
||||
info = order['info'];
|
||||
}
|
||||
const id = this.safeString (order, 'order_number');
|
||||
const fee = this.safeValue (order, 'fee'); // propagated from createOrder
|
||||
const status = (filledPercentage === 1.0) ? 'closed' : 'open';
|
||||
return {
|
||||
'id': id,
|
||||
'clientOrderId': undefined,
|
||||
'datetime': this.iso8601 (timestamp),
|
||||
'timestamp': timestamp,
|
||||
'lastTradeTimestamp': undefined,
|
||||
'status': status,
|
||||
'symbol': symbol,
|
||||
'type': 'limit',
|
||||
'side': order['type'],
|
||||
'price': price,
|
||||
'cost': cost,
|
||||
'amount': amount,
|
||||
'filled': filled,
|
||||
'remaining': remaining,
|
||||
'trades': undefined,
|
||||
'fee': fee,
|
||||
'info': info,
|
||||
'average': undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async createOrder (symbol, type, side, amount, price = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const method = 'privatePost' + this.capitalize (side);
|
||||
const request = {
|
||||
'market': market['id'],
|
||||
// 'price': this.priceToPrecision (symbol, price),
|
||||
// 'amount': this.amountToPrecision (symbol, amount),
|
||||
'price': price,
|
||||
'amount': amount,
|
||||
};
|
||||
const response = await this[method] (this.extend (request, params));
|
||||
const success = this.safeInteger (response, 'success');
|
||||
if (success !== 1) {
|
||||
throw new InvalidOrder (this.id + ' ' + this.json (response));
|
||||
}
|
||||
let parts = response['message'].split (' / ');
|
||||
parts = parts.slice (1);
|
||||
const feeParts = parts[5].split (' ');
|
||||
const order = this.parseOrder ({
|
||||
'timestamp': this.milliseconds (),
|
||||
'order_number': response['order_number'],
|
||||
'type': parts[0].toLowerCase (),
|
||||
'market': parts[0].toLowerCase (),
|
||||
'amount': parts[2].split (' ')[1],
|
||||
'price': parts[3].split (' ')[1],
|
||||
'total': parts[4].split (' ')[1],
|
||||
'fee': {
|
||||
'cost': parseFloat (feeParts[1]),
|
||||
'currency': feeParts[2],
|
||||
},
|
||||
'progress': '0.0',
|
||||
'info': response,
|
||||
}, market);
|
||||
const id = order['id'];
|
||||
this.orders[id] = order;
|
||||
return order;
|
||||
}
|
||||
|
||||
async cancelOrder (id, symbol = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const request = {
|
||||
'order_number': id,
|
||||
'market': market['id'],
|
||||
};
|
||||
return await this.privatePostCancelOrder (this.extend (request, params));
|
||||
}
|
||||
|
||||
async fetchOrder (id, symbol = undefined, params = {}) {
|
||||
if (symbol === undefined) {
|
||||
throw new ArgumentsRequired (this.id + ' fetchOrder() requires a symbol argument');
|
||||
}
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const request = {
|
||||
'order_number': id,
|
||||
'market': market['id'],
|
||||
};
|
||||
const response = await this.privatePostOrderStatus (this.extend (request, params));
|
||||
return this.parseOrder (response, market);
|
||||
}
|
||||
|
||||
async fetchOpenOrders (symbol = undefined, since = undefined, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const request = {
|
||||
'market': market['id'],
|
||||
};
|
||||
const response = await this.privatePostOpenOrders (this.extend (request, params));
|
||||
return this.parseOrders (response['order_open'], market, since, limit);
|
||||
}
|
||||
|
||||
async fetchMyTrades (symbol = undefined, since = undefined, limit = undefined, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const market = this.market (symbol);
|
||||
const request = {
|
||||
'market': market['id'],
|
||||
};
|
||||
const response = await this.privatePostTradeHistory (this.extend (request, params));
|
||||
return this.parseTrades (response['trade_history'], market, since, limit);
|
||||
}
|
||||
|
||||
async fetchDepositAddress (code, params = {}) {
|
||||
await this.loadMarkets ();
|
||||
const currency = this.currency (code);
|
||||
const request = {
|
||||
'currency': currency['id'],
|
||||
};
|
||||
const response = await this.privatePostDepositAddress (this.extend (request, params));
|
||||
const address = this.safeString (response, 'deposit_address');
|
||||
this.checkAddress (address);
|
||||
const tag = this.safeString (response, 'payment_id');
|
||||
return {
|
||||
'currency': code,
|
||||
'address': address,
|
||||
'tag': tag,
|
||||
'info': response,
|
||||
};
|
||||
}
|
||||
|
||||
sign (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) {
|
||||
let url = this.urls['api'] + '/' + api;
|
||||
let query = this.omit (params, this.extractParams (path));
|
||||
if (api === 'public') {
|
||||
url += '/' + this.implodeParams (path, params);
|
||||
if (Object.keys (query).length) {
|
||||
url += '?' + this.urlencode (query);
|
||||
}
|
||||
} else {
|
||||
this.checkRequiredCredentials ();
|
||||
query = this.extend ({
|
||||
'command': path,
|
||||
'nonce': this.nonce (),
|
||||
}, query);
|
||||
body = this.urlencode (query);
|
||||
const signature = this.hmac (this.encode (body), this.encode (this.secret), 'sha512');
|
||||
headers = {
|
||||
'Content-type': 'application/x-www-form-urlencoded',
|
||||
'Key': this.apiKey,
|
||||
'Sign': signature,
|
||||
};
|
||||
}
|
||||
return { 'url': url, 'method': method, 'body': body, 'headers': headers };
|
||||
}
|
||||
|
||||
async request (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) {
|
||||
const response = await this.fetch2 (path, api, method, params, headers, body);
|
||||
if ((typeof response === 'string') && (response.length < 1)) {
|
||||
throw new ExchangeError (this.id + ' returned empty response');
|
||||
}
|
||||
if ('success' in response) {
|
||||
const success = this.safeInteger (response, 'success');
|
||||
if (success === 0) {
|
||||
const message = this.safeString (response, 'message');
|
||||
if (message === 'Invalid APIKey') {
|
||||
throw new AuthenticationError (message);
|
||||
}
|
||||
throw new ExchangeError (message);
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
};
|
||||
Loaded 100 of 6483 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user