Beta 6 without Execution

This commit is contained in:
Luis Fernando Molina committed 2020-07-06 15:43:34 +02:00
1 parent 4033d4d07f
commit d40b985f5e
123 files changed
+8114 -12392

No files matched your search

@@ -0,0 +1,61 @@
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.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) {
try {
text = eval(announcement.formula.code)
} catch (err) {
tradingSystem.errors.push([announcement.formula.id, err.message])
}
}
if (bot.SESSION.socialBots !== undefined) {
bot.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.'])
}
}
}
}
}
}
+336
View File
@@ -0,0 +1,336 @@
exports.newSnapshots = function newSnapshots(bot, logger) {
/*
This module encapsulates the snapshots functionality.
*/
const MODULE_NAME = 'Snapshots'
let thisObject = {
reset: reset,
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.SESSION.parameters
}
function finalize() {
writeSnapshotFiles()
tradingEngine = undefined
tradingSystem = undefined
sessionParameters = undefined
positionKeys = undefined
strategyKeys = undefined
chart = undefined
}
function reset(pChart) {
/* This function helps reset data structures at every cycle of the simulation loop/=. */
chart = pChart // We need chat to be a local object accessible from conditions and formulas.
}
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 = bot.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)
}
let value = eval(instruction)
values.push(value)
}
}
}
}
} 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', 'ROI', 'Exit Type']
closeValues = [
tradingEngine.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.positionStatistics.hitFail.value, // Result
tradingEngine.current.position.positionStatistics.ROI.value, // ROI
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.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,168 @@
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 = {
cloneValues: cloneValues,
setCurrentCandle: setCurrentCandle,
updateEpisodeCountersAndStatistics: updateEpisodeCountersAndStatistics,
updateDistanceToEventsCounters: updateDistanceToEventsCounters,
initialize: initialize,
finalize: finalize
}
let tradingEngine
let sessionParameters
return thisObject
function initialize() {
/* Here we will go through all the nodes in the Trading Engine hiriarchy and apply the initial value to the value property when needed */
tradingEngine = bot.simulationState.tradingEngine
sessionParameters = bot.SESSION.parameters
if (bot.FIRST_EXECUTION === true) {
initializeNode(tradingEngine)
tradingEngine.current.balance.baseAsset.value = sessionParameters.sessionBaseAsset.config.initialBalance
tradingEngine.current.balance.quotedAsset.value = sessionParameters.sessionQuotedAsset.config.initialBalance
}
}
function finalize() {
tradingEngine = undefined
sessionParameters = undefined
}
function setCurrentCandle(candle) {
cloneValues(tradingEngine.current.candle, tradingEngine.previous.candle)
tradingEngine.current.candle.begin.value = candle.begin
tradingEngine.current.candle.end.value = candle.end
tradingEngine.current.candle.open.value = candle.open
tradingEngine.current.candle.close.value = candle.close
tradingEngine.current.candle.min.value = candle.min
tradingEngine.current.candle.max.value = candle.max
}
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 = bot.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 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
}
}
node.initialize = initializeNode // This will allow anyone to initialize this node and its children.
/* Now we go down through all this node children */
let nodeDefinition = bot.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
}
}
}
}
}
function updateEpisodeCountersAndStatistics() {
tradingEngine.episode.episodeCounters.periods.value++
tradingEngine.episode.episodeStatistics.days.value = tradingEngine.episode.episodeCounters.periods.value * sessionParameters.timeFrame.config.value / global.ONE_DAY_IN_MILISECONDS
}
function updateDistanceToEventsCounters() {
/* Keeping Distance Counters Up-to-date */
if (
tradingEngine.current.distanceToEvent.triggerOn.value > 0 // with this we avoind counting before the first event happens.
) {
tradingEngine.current.distanceToEvent.triggerOn.value++
}
if (
tradingEngine.current.distanceToEvent.triggerOff.value > 0 // with this we avoind counting before the first event happens.
) {
tradingEngine.current.distanceToEvent.triggerOff.value++
}
if (
tradingEngine.current.distanceToEvent.takePosition.value > 0 // with this we avoind counting before the first event happens.
) {
tradingEngine.current.distanceToEvent.takePosition.value++
}
if (
tradingEngine.current.distanceToEvent.closePosition.value > 0 // with this we avoind counting before the first event happens.
) {
tradingEngine.current.distanceToEvent.closePosition.value++
}
}
}
@@ -0,0 +1,319 @@
exports.newTradingExecution = function newTradingExecution(bot, logger) {
/*
The Trading Execution modules manages the execution of orders against the exchanges.
*/
const MODULE_NAME = 'Trading Execution'
let thisObject = {
takePosition: takePosition,
closePosition: closePosition,
initialize: initialize,
finalize: finalize
}
let tradingEngine
return thisObject
function initialize() {
tradingEngine = bot.simulationState.tradingEngine
}
function finalize() {
tradingEngine = undefined
}
function takePosition() {
/* Check that we are in LIVE MODE */
if (bot.SESSION.type === 'Live Trading Session' || bot.SESSION.type === 'Fordward Testion Session') {
logger.write(MODULE_NAME, '[PERSIST] runSimulation -> loop -> takePositionNow -> Taking a Position in ' + bot.SESSION.type)
logger.write(MODULE_NAME, '[PERSIST] runSimulation -> loop -> takePositionNow -> tradingEngine.current.position.size.value = ' + tradingEngine.current.position.size.value)
logger.write(MODULE_NAME, '[PERSIST] runSimulation -> loop -> takePositionNow -> tradingEngine.current.position.rate.value = ' + tradingEngine.current.position.rate.value)
logger.write(MODULE_NAME, '[PERSIST] runSimulation -> loop -> takePositionNow -> slippageAmount = ' + slippageAmount)
logger.write(MODULE_NAME, '[PERSIST] runSimulation -> loop -> takePositionNow -> tradingEngine.current.position.rate.value = ' + tradingEngine.current.position.rate.value)
/* We see if we need to put the actual order at the exchange. */
if (variable.executionContext !== undefined) {
switch (variable.executionContext.status) {
case 'Without a Position': { // We need to put the order because It was not put yet.
if (strategy.openStage !== undefined) {
if (strategy.openStage.openExecution !== undefined) {
putOpeningOrder()
return
}
}
break
}
case 'Position Closed': { // Waiting for a confirmation that the position was closed.
if (strategy.openStage !== undefined) {
if (strategy.openStage.openExecution !== undefined) {
putOpeningOrder()
return
}
}
break
}
case 'Taking Position': { // Waiting for a confirmation that the position was taken.
logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> takePositionNow -> Exiting code block because status is Taking Position.')
break
}
case 'In a Position': { // This should mean that we already put the order at the exchange.
logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> takePositionNow -> Exiting code block because status is In a Position.')
break
}
}
} else { // The context does not exist so it means we are not in a position.
if (strategy.openStage !== undefined) {
if (strategy.openStage.openExecution !== undefined) {
putOpeningOrder()
return
}
}
}
} else {
logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> takePositionNow -> Not trading live.')
}
}
function putOpeningOrder() {
logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> putOpeningOrder -> Entering function.')
/* We wont take a position unless we are withing the sessionParameters.timeRange.config.initialDatetime and the sessionParameters.timeRange.config.finalDatetime range */
if (sessionParameters.timeRange.config.initialDatetime !== undefined) {
if (candle.end < sessionParameters.timeRange.config.initialDatetime) {
logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> putOpeningOrder -> Not placing the trade at the exchange because current candle ends before the start date. -> sessionParameters.timeRange.config.initialDatetime = ' + sessionParameters.timeRange.config.initialDatetime)
takePositionAtSimulation()
return
}
}
/* We wont take a position if we are past the final datetime */
if (sessionParameters.timeRange.config.finalDatetime !== undefined) {
if (candle.begin > sessionParameters.timeRange.config.finalDatetime) {
logger.write(MODULE_NAME, '[INFO] runSimulation -> putOpeningOrder -> Not placing the trade at the exchange because current candle begins after the end date. -> sessionParameters.timeRange.config.finalDatetime = ' + sessionParameters.timeRange.config.finalDatetime)
takePositionAtSimulation()
return
}
}
/* Mechanism to avoid putting the same order over and over again at different executions of the simulation engine. */
if (variable.executionContext !== undefined) {
if (variable.executionContext.periods !== undefined) {
if (tradingEngine.episode.episodeCounters.periods.value <= variable.executionContext.periods) {
logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> putOpeningOrder -> Not placing the trade at the exchange because it was already placed at a previous execution.')
takePositionAtSimulation()
return
}
}
}
/* We are not going to place orders based on outdated information. The next filter prevents firing orders when backtesting. */
if (currentDay) {
let today = new Date(Math.trunc((new Date().valueOf()) / global.ONE_DAY_IN_MILISECONDS) * global.ONE_DAY_IN_MILISECONDS)
let processDay = new Date(Math.trunc(currentDay.valueOf() / global.ONE_DAY_IN_MILISECONDS) * global.ONE_DAY_IN_MILISECONDS)
if (today.valueOf() !== processDay.valueOf()) {
logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> putOpeningOrder -> Not placing the trade at the exchange because the current candle belongs to the previous day and that is considered simulation and not live trading.')
logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> putOpeningOrder -> today = ' + today)
logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> putOpeningOrder -> processDay = ' + processDay)
takePositionAtSimulation()
return
}
}
let orderPrice
let amountA
let amountB
let orderSide
if (bot.sessionAndMarketBaseAssetsAreEqual) {
orderSide = 'sell'
orderPrice = tradingEngine.current.position.rate.value
amountA = tradingEngine.current.position.size.value * orderPrice
amountB = tradingEngine.current.position.size.value
} else {
orderSide = 'buy'
orderPrice = tradingEngine.current.position.rate.value
amountA = tradingEngine.current.position.size.value
amountB = tradingEngine.current.position.size.value / orderPrice
}
variable.executionContext = {
status: 'Taking Position',
periods: tradingEngine.episode.episodeCounters.periods.value
}
logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> putOpeningOrder -> Ready to create order.')
exchangeAPI.createOrder(bot.market, orderSide, orderPrice, amountA, amountB, onOrderCreated)
function onOrderCreated(err, order) {
logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> putOpeningOrder -> onOrderCreated -> Entering function.')
try {
switch (err.result) {
case global.DEFAULT_OK_RESPONSE.result: {
logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> putOpeningOrder -> onOrderCreated -> DEFAULT_OK_RESPONSE ')
variable.executionContext = {
status: 'In a Position',
periods: tradingEngine.episode.episodeCounters.periods.value,
amountA: amountA,
amountB: amountB,
orderId: order.id
}
takePositionAtSimulation()
return
}
case global.DEFAULT_FAIL_RESPONSE.result: {
logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> putOpeningOrder -> onOrderCreated -> DEFAULT_FAIL_RESPONSE ')
logger.write(MODULE_NAME, '[ERROR] runSimulation -> loop -> putOpeningOrder -> onOrderCreated -> Message = ' + err.message)
strategy.openStage.openExecution.error = err.message
afterLoop()
return
}
case global.DEFAULT_RETRY_RESPONSE.result: {
logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> putOpeningOrder -> onOrderCreated -> DEFAULT_RETRY_RESPONSE ')
logger.write(MODULE_NAME, '[ERROR] runSimulation -> loop -> putOpeningOrder -> onOrderCreated -> Message = ' + err.message)
strategy.openStage.openExecution.error = err.message
afterLoop()
return
}
}
logger.write(MODULE_NAME, '[ERROR] runSimulation -> loop -> putOpeningOrder -> onOrderCreated -> Unexpected Response -> Message = ' + err.message)
callBackFunction(global.DEFAULT_FAIL_RESPONSE)
return
} catch (err) {
logger.write(MODULE_NAME, '[ERROR] runSimulation -> loop -> putOpeningOrder -> onOrderCreated -> err = ' + err.stack)
callBackFunction(global.DEFAULT_FAIL_RESPONSE)
return
}
}
}
function closePosition() {
/* Check that we are in LIVE MODE */
if (bot.SESSION.type === 'Live Trading Session' || bot.SESSION.type === 'Fordward Testion Session') {
/* We see if we need to put the actual order at the exchange. */
if (variable.executionContext !== undefined) {
switch (variable.executionContext.status) {
case 'Without a Position': { // No way to close anything at the exchange.
logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> Closing a Position -> Exiting code block because status is Without a Position.')
break
}
case 'In a Position': { // This should mean that we already put the order at the exchange.
if (strategy.closeStage !== undefined) {
if (strategy.closeStage.closeExecution !== undefined) {
putClosingOrder()
return
}
}
break
}
case 'Closing Position': { // Waiting for a confirmation that the position was taken.
if (strategy.closeStage !== undefined) {
if (strategy.closeStage.closeExecution !== undefined) {
putClosingOrder()
return
}
}
break
}
case 'Position Closed': { //
logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> Closing a Position -> Exiting code block because status is Position Closed.')
break
}
}
}
}
}
function putClosingOrder() {
/* Mechanism to avoid putting the same order over and over again at different executions of the simulation engine. */
if (variable.executionContext !== undefined) {
if (variable.executionContext.periods !== undefined) {
if (tradingEngine.episode.episodeCounters.periods.value <= variable.executionContext.periods) {
logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> putClosingOrder -> Exiting function because this closing was already submited at a previous execution.')
closePositionAtSimulation()
return
}
}
}
let orderPrice
let amountA
let amountB
let orderSide
if (bot.sessionAndMarketBaseAssetsAreEqual) {
orderSide = 'buy'
orderPrice = ticker.last + 100 // This is provisional and totally arbitrary, until we have a formula on the designer that defines this stuff.
amountA = tradingEngine.current.balance.quotedAsset.value
amountB = tradingEngine.current.balance.quotedAsset.value / orderPrice
} else {
orderSide = 'sell'
orderPrice = ticker.last - 100 // This is provisional and totally arbitrary, until we have a formula on the designer that defines this stuff.
amountA = tradingEngine.current.balance.baseAsset.value * orderPrice
amountB = tradingEngine.current.balance.baseAsset.value
}
variable.executionContext = {
status: 'Closing Position',
periods: tradingEngine.episode.episodeCounters.periods.value
}
logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> putClosingOrder -> About to close position at the exchange.')
exchangeAPI.createOrder(bot.market, orderSide, orderPrice, amountA, amountB, onOrderCreated)
function onOrderCreated(err, order) {
logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> putClosingOrder -> onOrderCreated -> Entering function.')
try {
switch (err.result) {
case global.DEFAULT_OK_RESPONSE.result: {
logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> putClosingOrder -> onOrderCreated -> DEFAULT_OK_RESPONSE ')
variable.executionContext = {
status: 'Position Closed',
periods: tradingEngine.episode.episodeCounters.periods.value,
amountA: amountA,
amountB: amountB,
orderId: order.id
}
closePositionAtSimulation()
return
}
case global.DEFAULT_FAIL_RESPONSE.result: {
logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> putClosingOrder -> onOrderCreated -> DEFAULT_FAIL_RESPONSE ')
/* We will assume that the problem is temporary, and expect that it will work at the next execution. */
strategy.closeStage.closeExecution.error = err.message
afterLoop()
return
}
case global.DEFAULT_RETRY_RESPONSE.result: {
logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> putOpeningOrder -> onOrderCreated -> DEFAULT_RETRY_RESPONSE ')
strategy.closeStage.closeExecution.error = err.message
afterLoop()
return
}
}
logger.write(MODULE_NAME, '[ERROR] runSimulation -> loop -> putClosingOrder -> onOrderCreated -> Unexpected Response -> Message = ' + err.message)
callBackFunction(global.DEFAULT_FAIL_RESPONSE)
return
} catch (err) {
logger.write(MODULE_NAME, '[ERROR] runSimulation -> loop -> putClosingOrder -> onOrderCreated -> err = ' + err.stack)
callBackFunction(global.DEFAULT_FAIL_RESPONSE)
return
}
}
}
}
@@ -0,0 +1,276 @@
exports.newTradingOutput = function newTradingOutput(bot, logger, 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 FULL_LOG = true
const MODULE_NAME = 'Trading Bot'
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(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(chart, timeFrame, timeFrameLabel, currentDay, callBackFunction) {
try {
bot.processingDailyFiles
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, UTILITIES)
let outputDatasets = bot.processNode.referenceParent.processOutput.outputDatasets
let market = bot.market
let totalFilesToBeRead = 0
let totalFilesRead = 0
let totalFilesToBeWritten = 0
let totalFilesWritten = 0
let outputDatasetsMap = new Map()
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
initializeOutputs()
} else {
readFiles()
}
return
function initializeOutputs() {
if (bot.processingDailyFiles) {
initializeDailyFiles()
} else {
initializeMarketFiles()
}
}
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, [])
}
}
runSimulation()
}
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, [])
}
}
runSimulation()
}
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) {
readDailyFiles()
} else {
readMarketFiles()
}
}
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.SESSION.folderName + '/' + dataset.parentNode.config.codeName + '/' + dataset.config.codeName + '/' + timeFrameLabel
readOutputFile(fileName, filePath, dataset.parentNode.config.codeName)
}
}
}
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 = currentDay.getUTCFullYear() + '/' + utilities.pad(currentDay.getUTCMonth() + 1, 2) + '/' + utilities.pad(currentDay.getUTCDate(), 2);
let fileName = 'Data.json'
let filePath = bot.filePathRoot + '/Output/' + bot.SESSION.folderName + '/' + dataset.parentNode.config.codeName + '/' + dataset.config.codeName + '/' + timeFrameLabel + "/" + dateForPath
readOutputFile(fileName, filePath, dataset.parentNode.config.codeName)
}
}
}
function readOutputFile(fileName, filePath, productName) {
totalFilesToBeRead++
filePath += '/' + fileName
fileStorage.getTextFile(filePath, onFileRead, true)
function onFileRead(err, text) {
try {
if (err.message === 'File does not exist.') {
outputDatasetsMap.set(productName, [])
anotherFileRead()
return
}
if (err.result !== global.DEFAULT_OK_RESPONSE.result) {
logger.write(MODULE_NAME, '[ERROR] start -> readMarketFiles -> readOutputFile -> onFileRead -> err = ' + err.stack)
logger.write(MODULE_NAME, '[ERROR] start -> readMarketFiles -> readOutputFile -> onFileRead -> filePath = ' + filePath)
logger.write(MODULE_NAME, '[ERROR] start -> readMarketFiles -> readOutputFile -> onFileRead -> market = ' + market.baseAsset + '_' + market.quotedAsset)
callBackFunction(err)
return
}
outputDatasetsMap.set(productName, JSON.parse(text))
anotherFileRead()
} catch (err) {
logger.write(MODULE_NAME, '[ERROR] start -> readMarketFiles -> readOutputFile -> onFileRead -> err = ' + err.stack)
callBackFunction(global.DEFAULT_FAIL_RESPONSE)
}
}
}
function anotherFileRead() {
totalFilesRead++
if (totalFilesRead === totalFilesToBeRead) {
runSimulation()
}
}
function runSimulation() {
tradingSimulation.runSimulation(
chart,
outputDatasetsMap,
writeFiles,
callBackFunction)
}
function writeFiles() {
/*
The output of files which were appended with information during the simulation execution, now needs to be saved.
*/
if (bot.processingDailyFiles) {
writeDailyFiles()
} else {
writeMarketFiles()
}
}
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.SESSION.folderName + '/' + dataset.parentNode.config.codeName + '/' + dataset.config.codeName + '/' + timeFrameLabel
writeOutputFile(fileName, filePath, dataset.parentNode.config.codeName)
}
}
}
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 = currentDay.getUTCFullYear() + '/' + utilities.pad(currentDay.getUTCMonth() + 1, 2) + '/' + utilities.pad(currentDay.getUTCDate(), 2);
let fileName = 'Data.json'
let filePath = bot.filePathRoot + '/Output/' + bot.SESSION.folderName + '/' + dataset.parentNode.config.codeName + '/' + dataset.config.codeName + '/' + timeFrameLabel + "/" + dateForPath
writeOutputFile(fileName, filePath, dataset.parentNode.config.codeName)
}
}
}
function writeOutputFile(fileName, filePath, productName) {
totalFilesToBeWritten++
filePath += '/' + fileName
let fileContent = JSON.stringify(outputDatasetsMap.get(productName))
fileStorage.createTextFile(filePath, fileContent, onFileCreated)
function onFileCreated(err) {
try {
if (err.result !== global.DEFAULT_OK_RESPONSE.result) {
logger.write(MODULE_NAME, '[ERROR] start -> readMarketFiles -> readOutputFile -> onFileRead -> err = ' + err.stack)
logger.write(MODULE_NAME, '[ERROR] start -> readMarketFiles -> readOutputFile -> onFileRead -> filePath = ' + filePath)
logger.write(MODULE_NAME, '[ERROR] start -> readMarketFiles -> readOutputFile -> onFileRead -> market = ' + market.baseAsset + '_' + market.quotedAsset)
callBackFunction(err)
return
}
anotherFileWritten()
} catch (err) {
logger.write(MODULE_NAME, '[ERROR] start -> readMarketFiles -> readOutputFile -> onFileRead -> err = ' + err.stack)
callBackFunction(global.DEFAULT_FAIL_RESPONSE)
}
}
}
function anotherFileWritten() {
totalFilesWritten++
if (totalFilesWritten === totalFilesToBeWritten) {
callBackFunction(global.DEFAULT_OK_RESPONSE)
}
}
} catch (err) {
logger.write(MODULE_NAME, '[ERROR] start -> err = ' + err.stack)
callBackFunction(global.DEFAULT_FAIL_RESPONSE)
}
}
}
@@ -0,0 +1,204 @@
exports.newTradingPosition = function newTradingPosition(bot, logger) {
/*
This module packages all functions related to Positions.
*/
const MODULE_NAME = 'Trading Position'
let thisObject = {
openPosition: openPosition,
closePosition: closePosition,
applyStopLossFormula: applyStopLossFormula,
applyTakeProfitFormula: applyTakeProfitFormula,
preventStopLossDistortion: preventStopLossDistortion,
applySlippageToStopLoss: applySlippageToStopLoss,
preventTakeProfitDistortion: preventTakeProfitDistortion,
applySlippageToTakeProfit: applySlippageToTakeProfit,
updateStopLoss: updateStopLoss,
updateTakeProfit: updateTakeProfit,
updateSizeAndRate: updateSizeAndRate,
applySlippageToRate: applySlippageToRate,
updateEnds: updateEnds,
updateStatus: updateStatus,
updateCounters: updateCounters,
updateStatistics: updateStatistics,
resetPosition: resetPosition,
initialize: initialize,
finalize: finalize
}
let tradingEngine
let sessionParameters
return thisObject
function initialize() {
tradingEngine = bot.simulationState.tradingEngine
sessionParameters = bot.SESSION.parameters
}
function finalize() {
tradingEngine = undefined
sessionParameters = undefined
}
function openPosition(situationName) {
tradingEngine.current.position.status.value = 'Open'
tradingEngine.current.position.begin.value = tradingEngine.current.candle.begin.value
tradingEngine.current.position.end.value = tradingEngine.current.candle.end.value
tradingEngine.current.position.situationName.value = situationName
tradingEngine.current.position.stopLoss.stopLossStage.value = 'Open Stage'
tradingEngine.current.position.takeProfit.takeProfitStage.value = 'Open Stage'
tradingEngine.current.position.stopLoss.stopLossPhase.value = 0
tradingEngine.current.position.takeProfit.takeProfitPhase.value = 0
}
function closePosition(exitType) {
tradingEngine.current.position.status.value = 'Closed'
tradingEngine.current.position.end.value = tradingEngine.current.candle.end.value
tradingEngine.current.position.exitType.value = exitType
/*
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.
*/
const TRADING_ENGINE_MODULE = require('./TradingEngine.js')
let tradingEngineModule = TRADING_ENGINE_MODULE.newTradingEngine(bot, logger)
tradingEngineModule.cloneValues(tradingEngine.current.position, tradingEngine.last.position)
}
function resetPosition() {
tradingEngine.current.position.initialize(tradingEngine.current.position)
}
function applyStopLossFormula(formulas, formulaId) {
tradingEngine.current.position.stopLoss.value = formulas.get(formulaId)
}
function applyTakeProfitFormula(formulas, formulaId) {
tradingEngine.current.position.takeProfit.value = formulas.get(formulaId)
}
function updateStopLoss(phase, stage) {
tradingEngine.current.position.stopLoss.stopLossPhase.value = phase
tradingEngine.current.position.stopLoss.stopLossStage.value = stage
}
function updateTakeProfit(phase, stage) {
tradingEngine.current.position.takeProfit.takeProfitPhase.value = phase
tradingEngine.current.position.takeProfit.takeProfitStage.value = stage
}
function updateSizeAndRate(size, rate) {
tradingEngine.current.position.size.value = size
tradingEngine.current.position.rate.value = rate
}
function applySlippageToRate() {
let slippageAmount = tradingEngine.current.position.rate.value * bot.SESSION.parameters.slippage.config.positionRate / 100
if (bot.sessionAndMarketBaseAssetsAreEqual) {
tradingEngine.current.position.beginRate.value = tradingEngine.current.position.rate.value - slippageAmount
} else {
tradingEngine.current.position.beginRate.value = tradingEngine.current.position.rate.value + slippageAmount
}
}
function preventStopLossDistortion() {
/*
Hit Point Validation
This prevents misscalculations when a formula places the stop loss in this case way beyond the market price.
If we take the stop loss value at those situation would be a huge distortion of facts.
*/
if (bot.sessionAndMarketBaseAssetsAreEqual) {
if (tradingEngine.current.position.stopLoss.value < tradingEngine.current.candle.min.value) {
tradingEngine.current.position.stopLoss.value = tradingEngine.current.candle.min.value
}
} else {
if (tradingEngine.current.position.stopLoss.value > tradingEngine.current.candle.max.value) {
tradingEngine.current.position.stopLoss.value = tradingEngine.current.candle.max.value
}
}
}
function applySlippageToStopLoss() {
let slippedStopLoss = tradingEngine.current.position.stopLoss.value
/* Apply the Slippage */
let slippageAmount = slippedStopLoss * bot.SESSION.parameters.slippage.config.stopLoss / 100
if (bot.sessionAndMarketBaseAssetsAreEqual) {
slippedStopLoss = slippedStopLoss + slippageAmount
} else {
slippedStopLoss = slippedStopLoss - slippageAmount
}
tradingEngine.current.position.endRate.value = slippedStopLoss
}
function preventTakeProfitDistortion() {
/*
Hit Point Validation:
This prevents misscalculations when a formula places the take profit in this case way beyond the market price.
If we take the stop loss value at those situation would be a huge distortion of facts.
*/
if (bot.sessionAndMarketBaseAssetsAreEqual) {
if (tradingEngine.current.position.takeProfit.value > tradingEngine.current.candle.max.value) {
tradingEngine.current.position.takeProfit.value = tradingEngine.current.candle.max.value
}
} else {
if (tradingEngine.current.position.takeProfit.value < tradingEngine.current.candle.min.value) {
tradingEngine.current.position.takeProfit.value = tradingEngine.current.candle.min.value
}
}
}
function applySlippageToTakeProfit() {
let slippedTakeProfit = tradingEngine.current.position.takeProfit.value
/* Apply the Slippage */
let slippageAmount = slippedTakeProfit * bot.SESSION.parameters.slippage.config.takeProfit / 100
if (bot.sessionAndMarketBaseAssetsAreEqual) {
slippedTakeProfit = slippedTakeProfit + slippageAmount
} else {
slippedTakeProfit = slippedTakeProfit - slippageAmount
}
tradingEngine.current.position.endRate.value = slippedTakeProfit
}
function updateEnds() {
if (tradingEngine.current.position.status.value === 'Open') {
tradingEngine.current.position.end.value = tradingEngine.current.candle.end.value
}
}
function updateStatus() {
if (tradingEngine.current.position.status.value === 'Closed') {
resetPosition()
}
}
function updateCounters() {
if (tradingEngine.current.position.status.value === 'Open') {
tradingEngine.current.position.positionCounters.periods.value++
}
}
function updateStatistics() {
if (bot.sessionAndMarketBaseAssetsAreEqual) {
tradingEngine.current.position.positionStatistics.profitLoss.value = tradingEngine.current.balance.baseAsset.value - tradingEngine.previous.balance.baseAsset.value
tradingEngine.current.position.positionStatistics.ROI.value = tradingEngine.current.position.positionStatistics.profitLoss.value * 100 / tradingEngine.current.position.size.value
} else {
tradingEngine.current.position.positionStatistics.profitLoss.value = tradingEngine.current.balance.quotedAsset.value - tradingEngine.previous.balance.quotedAsset.value
tradingEngine.current.position.positionStatistics.ROI.value = tradingEngine.current.position.positionStatistics.profitLoss.value * 100 / tradingEngine.current.position.size.value
}
tradingEngine.current.position.positionStatistics.days.value = tradingEngine.current.position.positionCounters.periods.value * sessionParameters.timeFrame.config.value / global.ONE_DAY_IN_MILISECONDS
if (tradingEngine.current.position.positionStatistics.ROI.value > 0) {
tradingEngine.current.position.positionStatistics.hitFail.value = 'HIT'
} else {
tradingEngine.current.position.positionStatistics.hitFail.value = 'FAIL'
}
}
}
@@ -0,0 +1,767 @@
exports.newTradingProcess = function newTradingProcess(bot, logger, UTILITIES, TRADING_OUTPUT_MODULE, USER_BOT_COMMONS) {
/*
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 LOG_FILE_CONTENT = false;
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();
let tradingOutputModule;
const FILE_STORAGE = require('../FileStorage.js');
let fileStorage = FILE_STORAGE.newFileStorage(logger);
let processConfig;
return thisObject;
function initialize(pProcessConfig, pStatusDependencies, pDataDependencies, callBackFunction) {
try {
logger.fileName = MODULE_NAME;
logger.initialize();
statusDependencies = pStatusDependencies;
dataDependenciesModule = pDataDependencies;
processConfig = pProcessConfig;
let TRADING_OUTPUT_MODULE = require("./TradingOutput")
tradingOutputModule = TRADING_OUTPUT_MODULE.newTradingOutput(bot, logger, UTILITIES, FILE_STORAGE);
tradingOutputModule.initialize(callBackFunction);
} catch (err) {
logger.write(MODULE_NAME, "[ERROR] initialize -> err = " + err.stack);
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
}
}
function finalize() {
dataFiles = undefined
multiPeriodDataFiles = undefined
statusDependencies = undefined
dataDependenciesModule = undefined
tradingOutputModule.finalize()
tradingOutputModule = undefined
fileStorage = undefined
processConfig = undefined
thisObject = undefined
}
function start(callBackFunction) {
try {
let currentTimeFrame
let currentTimeFrameLabel
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.
getContextVariables();
function getContextVariables() {
try {
let thisReport;
let reportKey;
let statusReport;
/* We are going to use the start date as beging of market date. */
contextVariables.dateBeginOfMarket = new Date(bot.SESSION.parameters.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());
/* Validation that the data is not up-to-date. */
if (bot.tradingProcessDate.valueOf() - global.ONE_DAY_IN_MILISECONDS > contextVariables.dateEndOfMarket.valueOf() && bot.SESSION.type !== "Backtesting Session") {
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> getContextVariables -> " + "Head of the market is @ " + contextVariables.dateEndOfMarket); }
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[ERROR] start -> getContextVariables -> You need to UPDATE your datasets in order to run a " + bot.SESSION.type) }
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
return;
}
/* 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);
processSingleFiles();
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); }
processSingleFiles();
}
} 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 processSingleFiles() {
let dependencyIndex = 0;
dataFiles = new Map();
dependencyLoopBody();
function dependencyLoopBody() {
let dependency = dataDependenciesModule.nodeArray[dependencyIndex];
let datasetModule = dataDependenciesModule.dataSetsModulesArray[dependencyIndex];
getFile();
function getFile() {
if (datasetModule.node.config.codeName !== "Single-File") {
dependencyControlLoop();
return
}
if (dataDependenciesModule.isItADepenency('atAnyTimeFrame', datasetModule.node.parentNode.config.singularVariableName) !== true) {
dependencyControlLoop();
return
}
let fileName = "Data.json";
let filePath = datasetModule.node.parentNode.config.codeName + '/' + datasetModule.node.config.codeName;
datasetModule.getTextFile(filePath, fileName, onFileReceived);
function onFileReceived(err, text) {
try {
if (LOG_FILE_CONTENT === true) { logger.write(MODULE_NAME, "[INFO] start -> processSingleFiles -> dependencyLoopBody -> getFile -> onFileReceived -> text = " + text); }
if (err.result !== global.DEFAULT_OK_RESPONSE.result) {
if (err.message === 'File does not exist.') {
logger.write(MODULE_NAME, "[ERROR] The file " + filePath + '/' + fileName + ' does not exist and it is required to continue. This process will retry to read it in a while. In the meantime make yourself sure that the process that generates it has ran properly.');
callBackFunction(global.DEFAULT_RETRY_RESPONSE);
} else {
logger.write(MODULE_NAME, "[ERROR] start -> processSingleFiles -> dependencyLoopBody -> getFile -> onFileReceived -> err = " + err.message);
callBackFunction(err);
}
return;
}
let dataFile = JSON.parse(text);
dataFiles.set(dependency.id, dataFile);
dependencyControlLoop();
}
catch (err) {
logger.write(MODULE_NAME, "[ERROR] start -> processSingleFiles -> dependencyLoopBody -> getFile -> onFileReceived -> err = " + err.stack);
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
}
}
}
}
function dependencyControlLoop() {
dependencyIndex++
if (dependencyIndex < dataDependenciesModule.nodeArray.length) {
dependencyLoopBody()
} else {
let mapKey = "Single Files"
multiPeriodDataFiles.set(mapKey, dataFiles)
processMarketFiles()
}
}
}
/*
We do market files first since if the simulation is run on daily files, there will be a loop to getch each daus files and we do not need
that loop to reload market files.
*/
function processMarketFiles() {
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];
let dependencyIndex = 0;
dataFiles = new Map();
if (bot.SESSION.parameters.timeFrame.config.label === timeFrameLabel) {
currentTimeFrame = global.marketFilesPeriods[n][0];
currentTimeFrameLabel = global.marketFilesPeriods[n][1];
}
dependencyLoopBody();
function dependencyLoopBody() {
let dependency = dataDependenciesModule.nodeArray[dependencyIndex];
let datasetModule = dataDependenciesModule.dataSetsModulesArray[dependencyIndex];
getFile();
function getFile() {
if (dependency.referenceParent.config.codeName !== "Multi-Period-Market") {
dependencyControlLoop();
return
}
if (dataDependenciesModule.isItADepenency(timeFrameLabel, datasetModule.node.parentNode.config.singularVariableName) !== true) {
if (!(bot.SESSION.parameters.timeFrame.config.label === timeFrameLabel && datasetModule.node.parentNode.config.pluralVariableName === 'candles')) {
dependencyControlLoop();
return
}
}
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) {
try {
if (err.result !== global.DEFAULT_OK_RESPONSE.result) {
if (err.message === 'File does not exist.') {
logger.write(MODULE_NAME, "[ERROR] The file " + filePath + '/' + fileName + ' does not exist and it is required to continue. This process will retry to read it in a while. In the meantime make yourself sure that the process that generates it has ran properly.');
callBackFunction(global.DEFAULT_RETRY_RESPONSE);
} else {
logger.write(MODULE_NAME, "[ERROR] start -> processMarketFiles -> timeFramesLoopBody -> dependencyLoopBody -> getFile -> onFileReceived -> err = " + err.message);
callBackFunction(err);
}
return;
}
let dataFile = JSON.parse(text);
let trimmedDataFile = trimDataFile(dataFile, datasetModule.node.parentNode.record)
dataFiles.set(dependency.id, trimmedDataFile);
dependencyControlLoop();
}
catch (err) {
logger.write(MODULE_NAME, "[ERROR] start -> processMarketFiles -> timeFramesLoopBody -> dependencyLoopBody -> getFile -> onFileReceived -> err = " + err.stack);
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
}
}
}
}
function dependencyControlLoop() {
dependencyIndex++;
if (dependencyIndex < dataDependenciesModule.nodeArray.length) {
dependencyLoopBody();
} else {
let mapKey = global.marketFilesPeriods[n][1];
multiPeriodDataFiles.set(mapKey, dataFiles)
timeFramesControlLoop();
}
}
}
function timeFramesControlLoop() {
n++;
if (n < global.marketFilesPeriods.length) {
timeFramesLoopBody();
} else {
processDailyFiles();
}
}
}
function processDailyFiles() {
let n = 0;
if (currentTimeFrame) {
buildCharts()
return
}
bot.tradingProcessDate = new Date(contextVariables.lastFile.valueOf() - global.ONE_DAY_IN_MILISECONDS); // Go back one day to start well when we advance time at the begining of the loop.
advanceTime();
function advanceTime() {
bot.tradingProcessDate = new Date(bot.tradingProcessDate.valueOf() + global.ONE_DAY_IN_MILISECONDS);
previousDay = new Date(bot.tradingProcessDate.valueOf() - global.ONE_DAY_IN_MILISECONDS);
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> processDailyFiles -> advanceTime -> bot.tradingProcessDate = " + bot.tradingProcessDate); }
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> processDailyFiles -> advanceTime -> previousDay = " + previousDay); }
/* Validation that we are not going past the user defined end date. */
if (bot.tradingProcessDate.valueOf() >= bot.SESSION.parameters.timeRange.config.finalDatetime) {
const logText = "User Defined End Datetime reached @ " + previousDay.getUTCFullYear() + "/" + (previousDay.getUTCMonth() + 1) + "/" + previousDay.getUTCDate() + ".";
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> processDailyFiles -> advanceTime -> " + logText); }
bot.SESSION.stop(logText)
callBackFunction(global.DEFAULT_OK_RESPONSE);
return;
}
/* Validation that we are not going past the head of the market. */
if (bot.tradingProcessDate.valueOf() > contextVariables.dateEndOfMarket.valueOf() + global.ONE_DAY_IN_MILISECONDS - 1) {
const logText = "Head of the market found @ " + previousDay.getUTCFullYear() + "/" + (previousDay.getUTCMonth() + 1) + "/" + previousDay.getUTCDate() + ".";
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> processDailyFiles -> advanceTime -> " + logText); }
callBackFunction(global.DEFAULT_OK_RESPONSE);
return;
}
checkStopTaskGracefully();
}
function checkStopTaskGracefully() {
/* Validation that we dont need to stop. */
if (global.STOP_TASK_GRACEFULLY === true) {
callBackFunction(global.DEFAULT_OK_RESPONSE);
return;
}
checkStopProcessing();
}
function checkStopProcessing() {
/* Validation that we dont need to stop. */
if (bot.STOP_SESSION === true) {
callBackFunction(global.DEFAULT_OK_RESPONSE);
return;
}
timeFramesLoop();
}
function timeFramesLoop() {
/* Telling the world we are alive and doing well */
let processingDate = bot.tradingProcessDate.getUTCFullYear() + '-' + utilities.pad(bot.tradingProcessDate.getUTCMonth() + 1, 2) + '-' + utilities.pad(bot.tradingProcessDate.getUTCDate(), 2);
bot.processHeartBeat(processingDate)
/*
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];
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) {
timeFramesControlLoop();
return;
}
}
if (bot.SESSION.parameters.timeFrame.config.label === timeFrameLabel) {
currentTimeFrame = global.dailyFilePeriods[n][0];
currentTimeFrameLabel = global.dailyFilePeriods[n][1];
}
let dependencyIndex = 0;
dataFiles = new Map();
dependencyLoopBody();
function dependencyLoopBody() {
let dependency = dataDependenciesModule.nodeArray[dependencyIndex];
let datasetModule = dataDependenciesModule.dataSetsModulesArray[dependencyIndex];
let file
getFile()
function getFile() {
if (dependency.referenceParent.config.codeName !== "Multi-Period-Daily") {
dependencyControlLoop();
return
}
if (dataDependenciesModule.isItADepenency(timeFrameLabel, datasetModule.node.parentNode.config.singularVariableName) !== true) {
if (!(bot.SESSION.parameters.timeFrame.config.label === timeFrameLabel && datasetModule.node.parentNode.config.pluralVariableName === 'candles')) {
dependencyControlLoop();
return
}
}
let dateForPath = bot.tradingProcessDate.getUTCFullYear() + '/' + utilities.pad(bot.tradingProcessDate.getUTCMonth() + 1, 2) + '/' + utilities.pad(bot.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";
datasetModule.getTextFile(filePath, fileName, onFileReceived);
function onFileReceived(err, text) {
try {
if ((err.result === "Fail Because" && err.message === "File does not exist.") || err.code === 'The specified key does not exist.') {
logger.write(MODULE_NAME, "[ERROR] The file " + filePath + '/' + fileName + ' does not exist and it is required to continue. This process will retry to read it in a while. In the meantime make yourself sure that the process that generates it has ran properly.');
callBackFunction(global.DEFAULT_RETRY_RESPONSE);
return;
}
if (err.result !== global.DEFAULT_OK_RESPONSE.result) {
logger.write(MODULE_NAME, "[ERROR] start -> processDailyFiles -> timeFramesLoopBody -> dependencyLoopBody -> getFile -> onFileReceived -> Not Ok -> err = " + err.code);
callBackFunction(err);
return;
}
file = JSON.parse(text);
dataFiles.set(dependency.id, file);
dependencyControlLoop();
}
catch (err) {
logger.write(MODULE_NAME, "[ERROR] start -> processDailyFiles -> timeFramesLoopBody -> dependencyLoopBody -> getFile -> onFileReceived -> err = " + err.stack);
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
}
}
}
}
function dependencyControlLoop() {
dependencyIndex++;
if (dependencyIndex < dataDependenciesModule.nodeArray.length) {
dependencyLoopBody();
} else {
let mapKey = global.dailyFilePeriods[n][1];
multiPeriodDataFiles.set(mapKey, dataFiles)
timeFramesControlLoop();
}
}
}
function timeFramesControlLoop() {
n++;
if (n < global.dailyFilePeriods.length) {
timeFramesLoopBody();
} else {
n = 0;
if (currentTimeFrame !== undefined) {
buildCharts(advanceTime);
} else {
logger.write(MODULE_NAME, "[ERROR] start -> processDailyFiles -> timeFramesControlLoop -> Time Frame not Recognized. Can not continue.");
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
}
}
}
}
function buildCharts(advanceTime) {
const COMMONS = require('../Commons.js')
let commons = COMMONS.newCommons(bot, logger, UTILITIES, FILE_STORAGE)
let chart = {}
let mainDependency = {}
/* The first phase here is about checking that we have everything we need at the definition level. */
let dataDependencies = bot.processNode.referenceParent.processDependencies.dataDependencies
if (commons.validateDataDependencies(dataDependencies, callBackFunction) !== true) { return }
let outputDatasets = bot.processNode.referenceParent.processOutput.outputDatasets
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
}
tradingOutputModule.start(
chart,
currentTimeFrame,
currentTimeFrameLabel,
bot.tradingProcessDate,
onOutputGenerated);
function onOutputGenerated(err) {
try {
if (err.result !== global.DEFAULT_OK_RESPONSE.result) {
callBackFunction(err);
return;
}
bot.FIRST_EXECUTION = false // From here on, all other loops executions wont be the first execution.
if (currentTimeFrame > global.dailyFilePeriods[0][0]) {
writeMarketStatusReport(onMarketStatusReport)
} else {
writeDataRanges(currentTimeFrameLabel, onWritten);
}
function onWritten(err) {
try {
if (err.result !== global.DEFAULT_OK_RESPONSE.result) {
logger.write(MODULE_NAME, "[ERROR] start -> processDailyFiles -> buildCharts -> onOutputGenerated -> onWritten -> err = " + err.stack);
callBackFunction(err);
return;
}
writeDailyStatusReport(bot.tradingProcessDate, onDailyStatusReport);
} catch (err) {
logger.write(MODULE_NAME, "[ERROR] start -> processDailyFiles -> buildCharts -> onOutputGenerated -> onWritten -> err = " + err.stack);
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
}
}
function onDailyStatusReport() {
advanceTime();
}
function onMarketStatusReport() {
/* This is where we check if we need reached the user defined end datetime. */
const now = new Date()
if (now.valueOf() >= bot.SESSION.parameters.timeRange.config.finalDatetime) {
const logText = "User Defined End Datetime reached @ " + now.getUTCFullYear() + "/" + (now.getUTCMonth() + 1) + "/" + now.getUTCDate() + ".";
if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> processDailyFiles -> buildCharts -> onOutputGenerated -> onMarketStatusReport -> " + logText); }
bot.SESSION.stop('because it just finished.')
callBackFunction(global.DEFAULT_OK_RESPONSE);
return;
}
if (bot.SESSION.type === 'Backtesting Session') {
bot.SESSION.stop('Backtesting Session over Market Files Finished.')
}
callBackFunction(global.DEFAULT_OK_RESPONSE);
}
}
catch (err) {
logger.write(MODULE_NAME, "[ERROR] start -> processDailyFiles -> buildCharts -> onOutputGenerated -> err = " + err.stack);
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
}
}
}
function writeDataRanges(currentTimeFrameLabel, callBack) {
let outputDatasetIndex = -1;
controlLoop();
function productLoopBody() {
let productCodeName = bot.processNode.referenceParent.processOutput.outputDatasets[outputDatasetIndex].referenceParent.parentNode.config.codeName;
writeDataRange(contextVariables.dateBeginOfMarket, bot.tradingProcessDate, productCodeName, currentTimeFrameLabel, controlLoop);
}
function controlLoop() {
outputDatasetIndex++;
if (outputDatasetIndex < bot.processNode.referenceParent.processOutput.outputDatasets.length) {
productLoopBody();
} else {
callBack(global.DEFAULT_OK_RESPONSE);
}
}
}
function writeDataRange(pBegin, pEnd, productCodeName, currentTimeFrameLabel, callBack) {
let dataRange = {
begin: pBegin.valueOf(),
end: pEnd.valueOf() + global.ONE_DAY_IN_MILISECONDS
};
let fileContent = JSON.stringify(dataRange);
let fileName = '/Data.Range.json';
let filePath = bot.filePathRoot + "/Output/" + bot.SESSION.folderName + "/" + productCodeName + "/" + 'Multi-Period-Daily' + 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;
}
if (LOG_FILE_CONTENT === true) {
logger.write(MODULE_NAME, "[INFO] start -> writeDataRange -> onFileCreated -> Content written = " + fileContent);
}
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)
callBack(global.DEFAULT_OK_RESPONSE);
}
}
function writeDailyStatusReport(lastFileDate, callBack) {
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 = lastFileDate;
thisReport.file.simulationState = bot.simulationState;
thisReport.save(callBack);
}
function writeMarketStatusReport(callBack) {
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;
logger.newInternalLoop(bot.codeName, bot.process, bot.tradingProcessDate);
thisReport.save(callBack);
}
function trimDataFile(dataFile, recordDefinition) {
/*
Here we will discard all the records in a file that are outside of the current time range.
*/
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 < bot.SESSION.parameters.timeRange.config.initialDatetime) { continue }
if (begin > bot.SESSION.parameters.timeRange.config.finalDatetime) { continue }
result.push(dataRecord)
}
return result
}
}
catch (err) {
logger.write(MODULE_NAME, "[ERROR] start -> err = " + err.stack);
callBackFunction(global.DEFAULT_FAIL_RESPONSE);
}
}
function pad(str, max) {
str = str.toString();
return str.length < max ? pad(" " + str, max) : str;
}
};
@@ -0,0 +1,264 @@
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.SESSION.parameters
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 = bot.processNode.referenceParent.processOutput.outputDatasets
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') {
persistRecord()
}
if (bot.processingDailyFiles === false && dataset.config.type === 'Market Files') {
persistRecord()
}
function persistRecord() {
pruneOutputFile(product, outputDatasetArray)
let record = scanRecordDefinition(product)
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]
/*
We check that the existing file does not contain at the end a record that
does not signal that the object is closed.
If it does, we replace it with the new one until at some point it will signal
does the object is closed.
*/
let lastRecord = outputDatasetArray[outputDatasetArray.length - 1]
if (lastRecord !== undefined) {
let lastRecordValue = lastRecord[j]
if (lastRecordValue !== product.config.propertyValueThatClosesObject) {
outputDatasetArray.splice(outputDatasetArray.length - 1, 1)
}
}
if (bot.processingDailyFiles) {
/*
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.candle.end.value)
let nextDay = new Date(tradingEngine.current.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. */
outputDatasetArray.push(record)
}
}
}
function scanRecordDefinition(product) {
/*
The Product Root Node is the root of the node hiriarchy branch that we need to look at
in order to fnd the node where we are going to extract the value.
*/
let productRootNode = eval(product.config.nodePath)
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) {
propertyRootNode = eval(recordProperty.config.nodePath)
}
/*
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
the any of the properties of the Root Node to get the Target Node.
*/
let targetNode = propertyRootNode[recordProperty.config.codeName]
/*
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) {
targetNode = targetNode[recordProperty.config.index]
}
/*
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) {
value = Number(value.toFixed(recordProperty.config.decimals))
}
} 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
}
}
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 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) {
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 >= tradingEngine.current.candle.end.value) {
outputFile.splice(i, 1)
pruneOutputFile(product, outputFile)
return
}
}
}
}
outputFile.isPrunned = true
}
}
@@ -0,0 +1,452 @@
exports.newTradingSimulation = function newTradingSimulation(bot, logger, 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.SESSION.name
let thisObject = {
finalize: finalize,
runSimulation: runSimulation
}
let utilities = UTILITIES.newCloudUtilities(bot, logger)
return thisObject
function finalize() {
thisObject = undefined
}
function runSimulation(
chart,
outputDatasetsMap,
callback,
callBackFunction) {
try {
let tradingSystem = bot.simulationState.tradingSystem
let tradingEngine = bot.simulationState.tradingEngine
let sessionParameters = bot.SESSION.parameters
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) }
/* Stop Loss Management TODO */
const MIN_STOP_LOSS_VALUE = 0.0000000001 // We can not let the stop be zero to avoid division by 0 error or infinity numbers as a result.
const MAX_STOP_LOSS_VALUE = Number.MAX_SAFE_INTEGER
/* Take Profit Management TODO */
const MIN_TAKE_PROFIT_VALUE = 0.0000000001 // We can not let the buy order be zero to avoid division by 0 error or infinity numbers as a result.
const MAX_TAKE_PROFIT_VALUE = Number.MAX_SAFE_INTEGER
/* Variables to know when we need to open or close a position. */
let triggeringOn
let triggeringOff
let takingPosition
let closingPosition
/* These are the Modules we will need to run the Simulation */
const EXCHANGE_API = require('../ExchangeAPI');
exchangeAPI = EXCHANGE_API.newExchangeAPI(logger, bot);
exchangeAPI.initialize();
const TRADING_ENGINE_MODULE = require('./TradingEngine.js')
let tradingEngineModule = TRADING_ENGINE_MODULE.newTradingEngine(bot, logger)
tradingEngineModule.initialize()
const TRADING_ENGINE_EXECUTION = require('./TradingExecution.js')
let tradingExecutionModule = TRADING_ENGINE_EXECUTION.newTradingExecution(bot, logger)
tradingExecutionModule.initialize()
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)
tradingSystemModule.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
/* This determines where we are standing */
bot.sessionAndMarketBaseAssetsAreEqual = (sessionParameters.sessionBaseAsset.referenceParent.referenceParent.config.codeName === bot.market.baseAsset)
initializeLoop()
function initializeLoop() {
if (bot.FIRST_EXECUTION === false && tradingEngine.last.candle !== undefined) {
/* Estimate the Initial Candle based on the last candle saved at tradingEngine */
let firstBegin = candles[0].begin
let lastBegin = tradingEngine.last.candle.begin
let diff = lastBegin - firstBegin
let amount = diff / sessionParameters.timeFrame.config.value
tradingEngine.current.candle.index.value = Math.trunc(amount) + 1 // Because we need to start from the next candle
if (tradingEngine.current.candle.index.value < 0 || tradingEngine.current.candle.index.value > candles.length - 1) {
logger.write(MODULE_NAME, '[ERROR] runSimulation -> initializeLoop -> Cannot resume simulation.')
logger.write(MODULE_NAME, '[ERROR] runSimulation -> initializeLoop -> tradingEngine.current.candle.index.value = ' + tradingEngine.current.candle.index.value)
logger.write(MODULE_NAME, '[ERROR] runSimulation -> initializeLoop -> firstBegin = ' + firstBegin)
logger.write(MODULE_NAME, '[ERROR] runSimulation -> initializeLoop -> lastBegin = ' + lastBegin)
logger.write(MODULE_NAME, '[ERROR] runSimulation -> initializeLoop -> diff = ' + diff)
logger.write(MODULE_NAME, '[ERROR] runSimulation -> initializeLoop -> amount = ' + amount)
callBackFunction(global.DEFAULT_FAIL_RESPONSE)
return
}
} else {
/* 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
tradingEngine.current.candle.index.value = Math.trunc(amount)
if (tradingEngine.current.candle.index.value < 0) { tradingEngine.current.candle.index.value = 0 }
if (tradingEngine.current.candle.index.value > 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.
*/
tradingEngine.current.candle.index.value = candles.length - 1
}
}
loop()
}
function loop() {
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> Processing candle # ' + tradingEngine.current.candle.index.value) }
let candle = candles[tradingEngine.current.candle.index.value] // This is the current candle the Simulation is working at.
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.
if (checkIfWeAreAtTheHeadOfTheMarket() === false) {
afterLoop()
return
}
if (checkMinimunAndMaximunBalance() === false) {
afterLoop()
return
}
if (checkInitialAndFinalDatetime() === false) {
controlLoop()
return
}
heartBeat()
positionChartAtCurrentCandle()
tradingSystemModule.reset(chart)
tradingSystemModule.mantainStrategies()
tradingSystemModule.mantainPositions()
tradingEngineModule.updateEpisodeCountersAndStatistics()
/*
The next step is about evaluationg Conditions and Formulas of the Trading System.
*/
tradingSystemModule.evalConditions()
tradingSystemModule.evalFormulas()
/*
Next we check if we will be triggering on, off or taking position.
*/
triggeringOn = tradingSystemModule.checkTriggerOn()
triggeringOff = tradingSystemModule.checkTriggerOff()
takingPosition = tradingSystemModule.checkTakePosition()
/* Stop Loss Management */
if (takingPosition !== true) {
tradingSystemModule.checkStopPhasesEvents()
tradingSystemModule.calculateStopLoss()
}
/* Take Profit Management */
if (takingPosition !== true) {
tradingSystemModule.checkTakeProfitPhaseEvents()
tradingSystemModule.calculateTakeProfit()
}
tradingEngineModule.updateDistanceToEventsCounters()
/* Checking if Stop or Take Profit were hit */
if (takingPosition !== true) {
closingPosition = tradingSystemModule.checkStopLossOrTakeProfitWasHit()
}
/* Taking a Position */
if (takingPosition === true) {
tradingSystemModule.getReadyToTakePosition()
/*
Whem we calculated all the formulas at the begining of the loop, we didn't know
we were going to take position in this same loop cycle. Some might formulas depends on
calculations that are triggered only when we are taking position. For that reason
we will calculate all formulas again, and after that the Stop Loss and Take Profit.
*/
tradingSystemModule.evalFormulas()
tradingSystemModule.calculateTakeProfit()
tradingSystemModule.calculateStopLoss()
tradingExecutionModule.takePosition()
tradingSystemModule.takePosition()
takingPosition = false
}
/* Closing a Position */
if (closingPosition === true) {
tradingSystemModule.getReadyToClosePosition()
tradingExecutionModule.closePosition()
tradingSystemModule.closePosition()
closingPosition = false
}
/* After a position was closed, we need to close the strategy. */
tradingSystemModule.exitStrategyAfterPosition()
tradingRecordsModule.appendRecords()
controlLoop()
}
function controlLoop() {
/* Checking if we should continue processing this loop or not. */
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.') }
console.log('[INFO] runSimulation -> controlLoop -> We are going to stop here bacause we were requested to stop processing this session.')
afterLoop()
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.') }
console.log('[INFO] runSimulation -> controlLoop -> We are going to stop here bacause we were requested to stop processing this task.')
afterLoop()
return
}
tradingEngine.current.candle.index.value++
if (tradingEngine.current.candle.index.value < candles.length) {
setImmediate(loop) // This will execute the next loop in the next iteration of the NodeJs event loop allowing for other callbacks to be executed.
} else {
afterLoop()
}
}
function afterLoop() {
tradingEngineModule.finalize()
tradingExecutionModule.finalize()
tradingSystemModule.finalize()
tradingRecordsModule.finalize()
callback()
}
function heartBeat() {
/* We will produce a simulation level heartbeat in order to inform the user this is running. */
heartBeatDate = new Date(Math.trunc(tradingEngine.current.candle.begin.value / global.ONE_DAY_IN_MILISECONDS) * global.ONE_DAY_IN_MILISECONDS)
if (heartBeatDate.valueOf() !== previousHeartBeatDate) {
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.sessionKey + ' Loop # ' + tradingEngine.current.candle.index.value + ' @ ' + processingDate) }
/* Telling the world we are alive and doing well */
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)
bot.processHeartBeat(currentDateString, percentage)
if (global.areEqualDates(currentDate, new Date()) === false) {
logger.newInternalLoop(bot.codeName, bot.process, currentDate, percentage)
}
}
previousHeartBeatDate = heartBeatDate.valueOf()
}
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 = bot.processNode.referenceParent.processDependencies.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
let currentElement = getElement(thisChart[pluralVariableName], 'Daily' + '-' + mapKey + '-' + pluralVariableName)
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
let currentElement = getElement(thisChart[pluralVariableName], 'Market' + '-' + mapKey + '-' + pluralVariableName)
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.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.candle.end.value
) {
let previousElement = pArray[i - 1]
if (previousElement.end < tradingEngine.current.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)
callBackFunction(global.DEFAULT_FAIL_RESPONSE)
}
}
function checkIfWeAreAtTheHeadOfTheMarket() {
/*
We skip the candle at the head of the market because it has not closed yet. The procedure to determine if we are at the head of the market is different
when we are processing Market Files than when we are processing Daily Files. TODO Check that the procedure is good for Beta 6
*/
if (bot.processingDailyFiles) { // We are processing Daily Files
let candlesPerDay = global.ONE_DAY_IN_MILISECONDS / sessionParameters.timeFrame.config.value
if (tradingEngine.current.candle.index.value === candles.length - 1) {
if ((candles.length < candlesPerDay) || (candles.length > candlesPerDay && candles.length < candlesPerDay * 2)) {
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> checkIfWeAreAtTheHeadOfTheMarket -> Skipping Candle because it is the last one and has not been closed yet.') }
return false
/*
Note here that in the last candle of the first day or the second day it will use an incomplete candle and partially calculated indicators.
if we skip these two periods, then there will be a hole in the file since the last period will be missing.
*/
}
}
} else { // We are processing Market Files
if (tradingEngine.current.candle.index.value === candles.length - 1) {
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> checkIfWeAreAtTheHeadOfTheMarket -> Skipping Candle because it is the last one and has not been closed yet.') }
return false
}
}
return true
}
function checkInitialAndFinalDatetime() {
/* Here we check that the current candle is not outside of user-defined time range at the session parameters.*/
if (tradingEngine.current.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
}
if (tradingEngine.current.candle.begin.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.stageType.value === 'No Stage' &&
tradingEngine.current.strategy.index.value === -1
) {
/*
First thing to do is to check if we are below the minimum balance, or above the maximun balance.
If we are, we are not gooing to trigger on again.
*/
let minimumBalance
let maximumBalance
let balance
if (bot.sessionAndMarketBaseAssetsAreEqual) {
balance = tradingEngine.current.balance.baseAsset.value
minimumBalance = sessionParameters.sessionBaseAsset.config.minimumBalance
maximumBalance = sessionParameters.sessionBaseAsset.config.maximumBalance
} else {
balance = tradingEngine.current.balance.quotedAsset.value
minimumBalance = sessionParameters.sessionQuotedAsset.config.minimumBalance
maximumBalance = sessionParameters.sessionQuotedAsset.config.maximumBalance
}
let stopRunningDate = new Date(tradingEngine.current.candle.begin.value)
if (balance <= minimumBalance) {
tradingSystem.error = 'Min Balance @ ' + stopRunningDate.toLocaleString()
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> checkMinimunAndMaximunBalance -> ' + tradingSystem.error) }
return false
}
if (balance >= maximumBalance) {
tradingSystem.error = 'Max Balance @ ' + stopRunningDate.toLocaleString()
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> checkMinimunAndMaximunBalance -> ' + tradingSystem.error) }
return false
}
}
return true
}
} catch (err) {
logger.write(MODULE_NAME, '[ERROR] runSimulation -> err = ' + err.stack)
callBackFunction(global.DEFAULT_FAIL_RESPONSE)
}
}
}
@@ -0,0 +1,83 @@
exports.newTradingStrategy = function newTradingStrategy(bot, logger) {
/*
This module packages all functions related to Strategies.
*/
const MODULE_NAME = 'Trading Strategy'
let thisObject = {
openStrategy: openStrategy,
closeStrategy: closeStrategy,
updateEnds: updateEnds,
updateStageType: updateStageType,
updateStatus: updateStatus,
updateCounters: updateCounters,
resetStrategy: resetStrategy,
initialize: initialize,
finalize: finalize
}
let tradingEngine
return thisObject
function initialize() {
tradingEngine = bot.simulationState.tradingEngine
}
function finalize() {
tradingEngine = undefined
}
function openStrategy(stageType, index, situationName, strategyName) {
tradingEngine.current.strategy.status.value = 'Open'
tradingEngine.current.strategy.begin.value = tradingEngine.current.candle.begin.value
tradingEngine.current.strategy.end.value = tradingEngine.current.candle.end.value
tradingEngine.current.strategy.beginRate.value = tradingEngine.current.candle.min.value
tradingEngine.current.strategy.endRate.value = tradingEngine.current.candle.min.value
tradingEngine.current.strategy.stageType.value = stageType
tradingEngine.current.strategy.index.value = index
tradingEngine.current.strategy.situationName.value = situationName
tradingEngine.current.strategy.strategyName.value = strategyName
}
function closeStrategy(exitType) {
tradingEngine.current.strategy.status.value = 'Closed'
tradingEngine.current.strategy.exitType.value = exitType
tradingEngine.current.strategy.end.value = tradingEngine.current.candle.end.value
tradingEngine.current.strategy.endRate.value = tradingEngine.current.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.
*/
const TRADING_ENGINE_MODULE = require('./TradingEngine.js')
let tradingEngineModule = TRADING_ENGINE_MODULE.newTradingEngine(bot, logger)
tradingEngineModule.cloneValues(tradingEngine.current.strategy, tradingEngine.last.strategy)
}
function resetStrategy() {
tradingEngine.current.strategy.initialize(tradingEngine.current.strategy)
}
function updateEnds() {
if (tradingEngine.current.strategy.status.value === 'Open') {
tradingEngine.current.strategy.end.value = tradingEngine.current.candle.end.value
tradingEngine.current.strategy.endRate.value = tradingEngine.current.candle.min.value
}
}
function updateStageType(stageType) {
tradingEngine.current.strategy.stageType.value = stageType
}
function updateStatus() {
if (tradingEngine.current.strategy.status.value === 'Closed') {
resetStrategy()
}
}
function updateCounters() {
if (tradingEngine.current.strategy.status.value === 'Open') {
tradingEngine.current.strategy.strategyCounters.periods.value++
}
}
}
@@ -0,0 +1,963 @@
exports.newTradingSystem = function newTradingSystem(bot, logger) {
/*
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 = {
reset: reset,
mantainStrategies: mantainStrategies,
mantainPositions: mantainPositions,
evalConditions: evalConditions,
evalFormulas: evalFormulas,
checkTriggerOn: checkTriggerOn,
checkTriggerOff: checkTriggerOff,
checkTakePosition: checkTakePosition,
checkStopPhasesEvents: checkStopPhasesEvents,
calculateStopLoss: calculateStopLoss,
checkTakeProfitPhaseEvents: checkTakeProfitPhaseEvents,
calculateTakeProfit: calculateTakeProfit,
checkStopLossOrTakeProfitWasHit: checkStopLossOrTakeProfitWasHit,
getReadyToTakePosition: getReadyToTakePosition,
takePosition: takePosition,
getReadyToClosePosition: getReadyToClosePosition,
closePosition: closePosition,
exitStrategyAfterPosition: exitStrategyAfterPosition,
getPositionSize: getPositionSize,
getPositionRate: getPositionRate,
initialize: initialize,
finalize: finalize
}
let chart
let tradingSystem
let tradingEngine
let sessionParameters
let conditions = new Map()
let formulas = new Map()
const TRADING_STRATEGY_MODULE = require('./TradingStrategy.js')
let tradingStrategyModule = TRADING_STRATEGY_MODULE.newTradingStrategy(bot, logger)
const TRADING_POSITION_MODULE = require('./TradingPosition.js')
let tradingPositionModule = TRADING_POSITION_MODULE.newTradingPosition(bot, logger)
const ANNOUNCEMENTS_MODULE = require('./Announcements.js')
let announcementsModule = ANNOUNCEMENTS_MODULE.newAnnouncements(bot, logger)
const SNAPSHOTS_MODULE = require('./Snapshots.js')
let snapshotsModule = SNAPSHOTS_MODULE.newSnapshots(bot, logger)
return thisObject
function initialize() {
tradingSystem = bot.simulationState.tradingSystem
tradingEngine = bot.simulationState.tradingEngine
sessionParameters = bot.SESSION.parameters
tradingStrategyModule.initialize()
tradingPositionModule.initialize()
announcementsModule.initialize()
snapshotsModule.initialize()
}
function finalize() {
tradingStrategyModule.finalize()
tradingStrategyModule = undefined
tradingPositionModule.finalize()
tradingPositionModule = undefined
announcementsModule.finalize()
announcementsModule = undefined
snapshotsModule.finalize()
snapshotsModule = undefined
chart = undefined
conditions = undefined
formulas = undefined
tradingSystem.highlights = undefined
tradingSystem.errors = undefined
tradingSystem.values = undefined
tradingSystem.status = undefined
tradingSystem.progress = undefined
tradingSystem.running = undefined
tradingSystem = undefined
tradingEngine = undefined
sessionParameters = undefined
}
function reset(pChart) {
/* This function helps reset data structures at every cycle of the simulation loop/=. */
chart = pChart // We need chat to be a local object accessible from conditions and formulas.
snapshotsModule.reset(chart)
tradingSystem.highlights = []
tradingSystem.errors = []
tradingSystem.values = []
tradingSystem.status = []
tradingSystem.progress = []
tradingSystem.running = []
tradingSystem.announcements = []
}
function mantainStrategies() {
tradingStrategyModule.updateStatus()
tradingStrategyModule.updateCounters()
tradingStrategyModule.updateEnds()
}
function mantainPositions() {
tradingPositionModule.updateStatus()
tradingPositionModule.updateCounters()
tradingPositionModule.updateEnds()
}
function evalConditions() {
evalNode(bot.simulationState.tradingSystem, 'Conditions')
}
function evalFormulas() {
evalNode(bot.simulationState.tradingSystem, 'Formulas')
}
function evalNode(node, evaluating) {
if (node === undefined) { return }
/* Here we check if there is a codition to be evaluated */
if (node.type === 'Condition' && evaluating === 'Conditions') {
/* We will eval this condition */
evalCondition(node)
}
/* Here we check if there is a formula to be evaluated */
if (node.type === 'Formula' && evaluating === 'Formulas') {
if (node.code !== undefined) {
/* We will eval this formula */
evalFormula(node)
}
}
/* Now we go down through all this node children */
let nodeDefinition = bot.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)
}
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)
}
}
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 -> 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
if (code.indexOf('previous') > -1 && err.message.indexOf('of undefined') > -1 ||
code.indexOf('chart') > -1 && err.message.indexOf('of undefined') > -1
) {
/*
We are not going to set an error for the casess we are using previous and the error is that the indicator is undefined.
*/
} else {
error = err.message
}
}
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 -> 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
if (node.code.indexOf('previous') > -1 && err.message.indexOf('of undefined') > -1 ||
node.code.indexOf('chart') > -1 && err.message.indexOf('of undefined') > -1
) {
/*
We are not going to set an error for the casess we are using previous and the error is that the indicator is undefined.
*/
} else {
error = err.message
}
}
formulas.set(node.id, value)
if (error !== undefined) {
tradingSystem.errors.push([node.id, error])
}
if (value !== undefined) {
tradingSystem.values.push([node.id, value])
}
logger.write(MODULE_NAME, '[INFO] evalFormula -> value = ' + value)
logger.write(MODULE_NAME, '[INFO] evalFormula -> error = ' + error)
}
function checkTriggerOn() {
if (
tradingEngine.current.strategy.index.value === tradingEngine.current.strategy.index.config.initialValue
) {
/*
To pick a new strategy we will check that any of the situations of the trigger on is true.
Once we enter into one strategy, we will ignore market conditions for others. However there is also
a strategy trigger off which can be hit before taking a position. If hit, we would
be outside a strategy again and looking for the conditions to enter all over again.
*/
for (let j = 0; j < tradingSystem.tradingStrategies.length; j++) {
if ( // If a strategy was already picked during the loop, we exit the loop
tradingEngine.current.strategy.index.value !== tradingEngine.current.strategy.index.config.initialValue
) { continue }
let strategy = tradingSystem.tradingStrategies[j]
let triggerStage = strategy.triggerStage
if (triggerStage !== undefined) {
if (triggerStage.triggerOn !== undefined) {
for (let k = 0; k < triggerStage.triggerOn.situations.length; k++) {
let situation = triggerStage.triggerOn.situations[k]
let passed
if (situation.conditions.length > 0) {
passed = true
}
for (let m = 0; m < situation.conditions.length; m++) {
let condition = situation.conditions[m]
let value = false
if (conditions.get(condition.id) !== undefined) {
value = conditions.get(condition.id)
}
if (value !== true) { passed = false }
}
tradingSystem.values.push([situation.id, passed])
if (passed) {
tradingSystem.highlights.push(situation.id)
tradingSystem.highlights.push(triggerStage.triggerOn.id)
tradingSystem.highlights.push(triggerStage.id)
tradingStrategyModule.openStrategy('Trigger Stage', j, situation.name, strategy.name)
tradingEngine.current.distanceToEvent.triggerOn.value = 1
announcementsModule.makeAnnoucements(triggerStage.triggerOn)
announcementsModule.makeAnnoucements(triggerStage)
if (bot.SESSION.type === 'Backtesting Session') {
if (sessionParameters.snapshots !== undefined) {
if (sessionParameters.snapshots.config.strategy === true) {
snapshotsModule.strategyEntry()
}
}
}
logger.write(MODULE_NAME, '[INFO] checkTriggerOn -> Entering into Strategy: ' + strategy.name)
return true // This Means that we have just met the conditions to trigger on.
}
}
}
}
}
}
return false // This Means that we have not met the conditions to trigger on.
}
function checkTriggerOff() {
if (tradingEngine.current.strategy.stageType.value === 'Trigger Stage') {
let strategy = tradingSystem.tradingStrategies[tradingEngine.current.strategy.index.value]
let triggerStage = strategy.triggerStage
if (triggerStage !== undefined) {
if (triggerStage.triggerOff !== undefined) {
for (let k = 0; k < triggerStage.triggerOff.situations.length; k++) {
let situation = triggerStage.triggerOff.situations[k]
let passed
if (situation.conditions.length > 0) {
passed = true
}
for (let m = 0; m < situation.conditions.length; m++) {
let condition = situation.conditions[m]
let value = false
if (conditions.get(condition.id) !== undefined) {
value = conditions.get(condition.id)
}
if (value !== true) { passed = false }
}
tradingSystem.values.push([situation.id, passed])
if (passed) {
tradingSystem.highlights.push(situation.id)
tradingSystem.highlights.push(triggerStage.triggerOff.id)
tradingSystem.highlights.push(triggerStage.id)
tradingStrategyModule.closeStrategy('Trigger Off')
tradingEngine.current.distanceToEvent.triggerOff.value = 1
announcementsModule.makeAnnoucements(triggerStage.triggerOff)
logger.write(MODULE_NAME, '[INFO] checkTriggerOff -> Closing Strategy: ' + strategy.name)
return true // This Means that we have just met the conditions to trigger off.
}
}
}
}
}
return false // This Means that we have not met the conditions to trigger off.
}
function checkTakePosition() {
/* Take Position Condition */
if (
tradingEngine.current.strategy.stageType.value === 'Trigger Stage' &&
tradingEngine.current.strategy.status.value === 'Open'
) {
let strategy = tradingSystem.tradingStrategies[tradingEngine.current.strategy.index.value]
let triggerStage = strategy.triggerStage
if (triggerStage !== undefined) {
if (triggerStage.takePosition !== undefined) {
for (let k = 0; k < triggerStage.takePosition.situations.length; k++) {
let situation = triggerStage.takePosition.situations[k]
let passed
if (situation.conditions.length > 0) {
passed = true
}
for (let m = 0; m < situation.conditions.length; m++) {
let condition = situation.conditions[m]
let value = false
if (conditions.get(condition.id) !== undefined) {
value = conditions.get(condition.id)
}
if (value !== true) { passed = false }
}
tradingSystem.values.push([situation.id, passed])
if (passed) {
tradingSystem.highlights.push(situation.id)
tradingSystem.highlights.push(triggerStage.takePosition.id)
tradingSystem.highlights.push(triggerStage.id)
tradingStrategyModule.updateStageType('Open Stage')
tradingPositionModule.openPosition(situation.name)
announcementsModule.makeAnnoucements(triggerStage.takePosition)
announcementsModule.makeAnnoucements(strategy.openStage)
if (bot.SESSION.type === 'Backtesting Session') {
if (sessionParameters.snapshots !== undefined) {
if (sessionParameters.snapshots.config.position === true) {
snapshotsModule.positionEntry()
}
}
}
logger.write(MODULE_NAME, '[INFO] checkTakePosition -> Conditions at the Take Position Event were met.')
return true // This Means that we have just met the conditions to take position.
}
}
}
}
}
return false // This Means that we have not met the conditions to take position.
}
function checkStopPhasesEvents() {
if (tradingEngine.current.strategy.stageType.value === 'Open Stage' || tradingEngine.current.strategy.stageType.value === 'Manage Stage') {
let strategy = tradingSystem.tradingStrategies[tradingEngine.current.strategy.index.value]
let openStage = strategy.openStage
let manageStage = strategy.manageStage
let parentNode
let phaseIndex
let phase
let stopLoss
let stage
if (tradingEngine.current.position.stopLoss.stopLossStage.value === 'Open Stage' && openStage !== undefined) {
stage = openStage
if (openStage.initialDefinition !== undefined) {
if (openStage.initialDefinition.initialStopLoss !== undefined) {
parentNode = openStage.initialDefinition
phaseIndex = tradingEngine.current.position.stopLoss.stopLossPhase.value
stopLoss = openStage.initialDefinition.initialStopLoss
phase = stopLoss.phases[phaseIndex]
}
}
}
if (tradingEngine.current.position.stopLoss.stopLossStage.value === 'Manage Stage' && manageStage !== undefined) {
stage = manageStage
if (manageStage.managedStopLoss !== undefined) {
parentNode = manageStage
phaseIndex = tradingEngine.current.position.stopLoss.stopLossPhase.value - 1
stopLoss = manageStage.managedStopLoss
phase = stopLoss.phases[phaseIndex]
}
}
if (parentNode !== undefined) {
if (phase === undefined) { return } // trying to jump to a phase that does not exists.
/* Check the next Phase Event. */
let nextPhaseEvent = phase.nextPhaseEvent
if (nextPhaseEvent !== undefined) {
for (let k = 0; k < nextPhaseEvent.situations.length; k++) {
let situation = nextPhaseEvent.situations[k]
let passed
if (situation.conditions.length > 0) {
passed = true
}
for (let m = 0; m < situation.conditions.length; m++) {
let condition = situation.conditions[m]
let value = false
if (conditions.get(condition.id) !== undefined) {
value = conditions.get(condition.id)
}
if (value !== true) { passed = false }
}
tradingSystem.values.push([situation.id, passed])
if (passed) {
tradingSystem.highlights.push(situation.id)
tradingSystem.highlights.push(nextPhaseEvent.id)
tradingSystem.highlights.push(phase.id)
tradingSystem.highlights.push(stopLoss.id)
tradingSystem.highlights.push(parentNode.id)
tradingSystem.highlights.push(stage.id)
tradingPositionModule.updateStopLoss(tradingEngine.current.position.stopLoss.stopLossPhase.value + 1, 'Manage Stage')
if (tradingEngine.current.position.takeProfit.takeProfitPhase.value > 0) {
tradingStrategyModule.updateStageType('Manage Stage')
announcementsModule.makeAnnoucements(manageStage)
}
announcementsModule.makeAnnoucements(nextPhaseEvent)
return // only one event can pass at the time
}
}
}
/* Check the Move to Phase Events. */
for (let n = 0; n < phase.moveToPhaseEvents.length; n++) {
let moveToPhaseEvent = phase.moveToPhaseEvents[n]
if (moveToPhaseEvent !== undefined) {
for (let k = 0; k < moveToPhaseEvent.situations.length; k++) {
let situation = moveToPhaseEvent.situations[k]
let passed
if (situation.conditions.length > 0) {
passed = true
}
for (let m = 0; m < situation.conditions.length; m++) {
let condition = situation.conditions[m]
let value = false
if (conditions.get(condition.id) !== undefined) {
value = conditions.get(condition.id)
}
if (value !== true) { passed = false }
}
tradingSystem.values.push([situation.id, passed])
if (passed) {
tradingSystem.highlights.push(situation.id)
tradingSystem.highlights.push(moveToPhaseEvent.id)
tradingSystem.highlights.push(phase.id)
tradingSystem.highlights.push(stopLoss.id)
tradingSystem.highlights.push(parentNode.id)
tradingSystem.highlights.push(stage.id)
let moveToPhase = moveToPhaseEvent.referenceParent
if (moveToPhase !== undefined) {
for (let q = 0; q < stopLoss.phases.length; q++) {
if (stopLoss.phases[q].id === moveToPhase.id) {
tradingPositionModule.updateStopLoss(q + 1, 'Manage Stage')
}
}
} else {
tradingSystem.errors.push([moveToPhaseEvent.id, 'This Node needs to reference a Phase.'])
continue
}
if (tradingEngine.current.position.takeProfit.takeProfitPhase.value > 0) {
tradingStrategyModule.updateStageType('Manage Stage')
announcementsModule.makeAnnoucements(manageStage)
}
announcementsModule.makeAnnoucements(moveToPhaseEvent)
return // only one event can pass at the time
}
}
}
}
}
}
}
function calculateStopLoss() {
if (tradingEngine.current.strategy.stageType.value === 'Open Stage' || tradingEngine.current.strategy.stageType.value === 'Manage Stage') {
let strategy = tradingSystem.tradingStrategies[tradingEngine.current.strategy.index.value]
let openStage = strategy.openStage
let manageStage = strategy.manageStage
let phase
if (tradingEngine.current.position.stopLoss.stopLossStage.value === 'Open Stage' && openStage !== undefined) {
if (openStage.initialDefinition !== undefined) {
if (openStage.initialDefinition.initialStopLoss !== undefined) {
phase = openStage.initialDefinition.initialStopLoss.phases[tradingEngine.current.position.stopLoss.stopLossPhase.value]
}
}
}
if (tradingEngine.current.position.stopLoss.stopLossStage.value === 'Manage Stage' && manageStage !== undefined) {
if (manageStage.managedStopLoss !== undefined) {
phase = manageStage.managedStopLoss.phases[tradingEngine.current.position.stopLoss.stopLossPhase.value - 1]
}
}
if (phase !== undefined) {
if (phase.formula !== undefined) {
let previousValue = tradingEngine.current.position.stopLoss.value
tradingPositionModule.applyStopLossFormula(formulas, phase.formula.id)
if (tradingEngine.current.position.stopLoss.value !== previousValue) {
announcementsModule.makeAnnoucements(phase)
}
}
}
}
}
function checkTakeProfitPhaseEvents() {
if (tradingEngine.current.strategy.stageType.value === 'Open Stage' || tradingEngine.current.strategy.stageType.value === 'Manage Stage') {
let strategy = tradingSystem.tradingStrategies[tradingEngine.current.strategy.index.value]
let openStage = strategy.openStage
let manageStage = strategy.manageStage
let parentNode
let phaseIndex
let phase
let takeProfit
let stage
if (tradingEngine.current.position.takeProfit.takeProfitStage.value === 'Open Stage' && openStage !== undefined) {
stage = openStage
if (openStage.initialDefinition !== undefined) {
if (openStage.initialDefinition.initialTakeProfit !== undefined) {
parentNode = openStage.initialDefinition
phaseIndex = tradingEngine.current.position.takeProfit.takeProfitPhase.value
takeProfit = openStage.initialDefinition.initialTakeProfit
phase = takeProfit.phases[phaseIndex]
}
}
}
if (tradingEngine.current.position.takeProfit.takeProfitStage.value === 'Manage Stage' && manageStage !== undefined) {
stage = manageStage
if (manageStage.managedTakeProfit !== undefined) {
parentNode = manageStage
phaseIndex = tradingEngine.current.position.takeProfit.takeProfitPhase.value - 1
takeProfit = manageStage.managedTakeProfit
phase = takeProfit.phases[phaseIndex]
}
}
if (parentNode !== undefined) {
if (phase === undefined) { return } // trying to jump to a phase that does not exists.
/* Check the next Phase Event. */
let nextPhaseEvent = phase.nextPhaseEvent
if (nextPhaseEvent !== undefined) {
for (let k = 0; k < nextPhaseEvent.situations.length; k++) {
let situation = nextPhaseEvent.situations[k]
let passed
if (situation.conditions.length > 0) {
passed = true
}
for (let m = 0; m < situation.conditions.length; m++) {
let condition = situation.conditions[m]
let value = false
if (conditions.get(condition.id) !== undefined) {
value = conditions.get(condition.id)
}
if (value !== true) { passed = false }
}
tradingSystem.values.push([situation.id, passed])
if (passed) {
tradingSystem.highlights.push(situation.id)
tradingSystem.highlights.push(nextPhaseEvent.id)
tradingSystem.highlights.push(phase.id)
tradingSystem.highlights.push(takeProfit.id)
tradingSystem.highlights.push(parentNode.id)
tradingSystem.highlights.push(stage.id)
tradingPositionModule.updateTakeProfit(tradingEngine.current.position.takeProfit.takeProfitPhase.value + 1, 'Manage Stage')
if (tradingEngine.current.position.stopLoss.stopLossPhase.value > 0) {
tradingStrategyModule.updateStageType('Manage Stage')
announcementsModule.makeAnnoucements(manageStage)
}
announcementsModule.makeAnnoucements(nextPhaseEvent)
return // only one event can pass at the time
}
}
}
/* Check the Move to Phase Events. */
for (let n = 0; n < phase.moveToPhaseEvents.length; n++) {
let moveToPhaseEvent = phase.moveToPhaseEvents[n]
if (moveToPhaseEvent !== undefined) {
for (let k = 0; k < moveToPhaseEvent.situations.length; k++) {
let situation = moveToPhaseEvent.situations[k]
let passed
if (situation.conditions.length > 0) {
passed = true
}
for (let m = 0; m < situation.conditions.length; m++) {
let condition = situation.conditions[m]
let value = false
if (conditions.get(condition.id) !== undefined) {
value = conditions.get(condition.id)
}
if (value !== true) { passed = false }
}
tradingSystem.values.push([situation.id, passed])
if (passed) {
tradingSystem.highlights.push(situation.id)
tradingSystem.highlights.push(moveToPhaseEvent.id)
tradingSystem.highlights.push(phase.id)
tradingSystem.highlights.push(takeProfit.id)
tradingSystem.highlights.push(parentNode.id)
tradingSystem.highlights.push(stage.id)
let moveToPhase = moveToPhaseEvent.referenceParent
if (moveToPhase !== undefined) {
for (let q = 0; q < takeProfit.phases.length; q++) {
if (takeProfit.phases[q].id === moveToPhase.id) {
tradingPositionModule.updateTakeProfit(q + 1, 'Manage Stage')
}
}
} else {
tradingSystem.errors.push([moveToPhaseEvent.id, 'This Node needs to reference a Phase.'])
continue
}
if (tradingEngine.current.position.stopLoss.stopLossPhase.value > 0) {
tradingStrategyModule.updateStageType('Manage Stage')
announcementsModule.makeAnnoucements(manageStage)
}
announcementsModule.makeAnnoucements(moveToPhaseEvent)
return // only one event can pass at the time
}
}
}
}
}
}
}
function calculateTakeProfit() {
if (tradingEngine.current.strategy.stageType.value === 'Open Stage' || tradingEngine.current.strategy.stageType.value === 'Manage Stage') {
let strategy = tradingSystem.tradingStrategies[tradingEngine.current.strategy.index.value]
let openStage = strategy.openStage
let manageStage = strategy.manageStage
let phase
if (tradingEngine.current.position.takeProfit.takeProfitStage.value === 'Open Stage' && openStage !== undefined) {
if (openStage.initialDefinition !== undefined) {
if (openStage.initialDefinition.initialTakeProfit !== undefined) {
phase = openStage.initialDefinition.initialTakeProfit.phases[tradingEngine.current.position.takeProfit.takeProfitPhase.value]
}
}
}
if (tradingEngine.current.position.takeProfit.takeProfitStage.value === 'Manage Stage' && manageStage !== undefined) {
if (manageStage.managedTakeProfit !== undefined) {
phase = manageStage.managedTakeProfit.phases[tradingEngine.current.position.takeProfit.takeProfitPhase.value - 1]
}
}
if (phase !== undefined) {
if (phase.formula !== undefined) {
let previousValue = tradingEngine.current.position.takeProfit.value
tradingPositionModule.applyTakeProfitFormula(formulas, phase.formula.id)
if (tradingEngine.current.position.takeProfit.value !== previousValue) {
announcementsModule.makeAnnoucements(phase)
}
}
}
}
}
function checkStopLossOrTakeProfitWasHit() {
{
if (tradingEngine.current.strategy.stageType.value === 'Open Stage' || tradingEngine.current.strategy.stageType.value === 'Manage Stage') {
let strategy = tradingSystem.tradingStrategies[tradingEngine.current.strategy.index.value]
/*
Checking what happened since the last execution. We need to know if the Stop Loss
or our Take Profit were hit.
*/
/* Stop Loss condition: Here we verify if the Stop Loss was hitted or not. */
if (
(bot.sessionAndMarketBaseAssetsAreEqual && tradingEngine.current.candle.max.value >= tradingEngine.current.position.stopLoss.value) ||
(!bot.sessionAndMarketBaseAssetsAreEqual && tradingEngine.current.candle.min.value <= tradingEngine.current.position.stopLoss.value)
) {
logger.write(MODULE_NAME, '[INFO] checkStopLossOrTakeProfitWasHit -> Stop Loss was hit.')
tradingPositionModule.preventStopLossDistortion()
tradingPositionModule.applySlippageToStopLoss()
tradingPositionModule.closePosition('Stop Loss')
tradingStrategyModule.updateStageType('Close Stage')
announcementsModule.makeAnnoucements(strategy.closeStage)
return true // This means that the STOP was hit.
}
/* Take Profit condition: Here we verify if the Take Profit was hit or not. */
if (
(bot.sessionAndMarketBaseAssetsAreEqual && tradingEngine.current.candle.min.value <= tradingEngine.current.position.takeProfit.value) ||
(!bot.sessionAndMarketBaseAssetsAreEqual && tradingEngine.current.candle.max.value >= tradingEngine.current.position.takeProfit.value)
) {
logger.write(MODULE_NAME, '[INFO] checkStopLossOrTakeProfitWasHit -> Take Profit was hit.')
tradingPositionModule.preventTakeProfitDistortion()
tradingPositionModule.applySlippageToTakeProfit()
tradingPositionModule.closePosition('Take Profit')
tradingStrategyModule.updateStageType('Close Stage')
announcementsModule.makeAnnoucements(strategy.closeStage)
return true // This means that the Take Profit was hit.
}
}
return false // This means that the neither the STOP nor the Take Profit were hit.
}
}
function getReadyToTakePosition() {
/* Updating Episode Counters */
tradingEngine.episode.episodeCounters.positions.value++
/* Inicializing this counter */
tradingEngine.current.distanceToEvent.takePosition.value = 1
/* Position size and rate */
tradingPositionModule.updateSizeAndRate(getPositionSize(), getPositionRate())
/* We take what was calculated at the formula of position rate and apply the slippage. */
tradingPositionModule.applySlippageToRate()
}
function takePosition() {
tradingEngine.previous.balance.baseAsset.value = tradingEngine.current.balance.baseAsset.value
tradingEngine.previous.balance.quotedAsset.value = tradingEngine.current.balance.quotedAsset.value
let feePaid = 0
if (bot.sessionAndMarketBaseAssetsAreEqual) {
feePaid = tradingEngine.current.position.size.value * tradingEngine.current.position.rate.value * bot.SESSION.parameters.feeStructure.config.taker / 100
tradingEngine.current.balance.quotedAsset.value = tradingEngine.current.balance.quotedAsset.value + tradingEngine.current.position.size.value * tradingEngine.current.position.rate.value - feePaid
tradingEngine.current.balance.baseAsset.value = tradingEngine.current.balance.baseAsset.value - tradingEngine.current.position.size.value
} else {
feePaid = tradingEngine.current.position.size.value / tradingEngine.current.position.rate.value * bot.SESSION.parameters.feeStructure.config.taker / 100
tradingEngine.current.balance.baseAsset.value = tradingEngine.current.balance.baseAsset.value + tradingEngine.current.position.size.value / tradingEngine.current.position.rate.value - feePaid
tradingEngine.current.balance.quotedAsset.value = tradingEngine.current.balance.quotedAsset.value - tradingEngine.current.position.size.value
}
}
function getReadyToClosePosition() {
/* Inicializing this counter */
tradingEngine.current.distanceToEvent.closePosition.value = 1
/* Position size and rate */
let strategy = tradingSystem.tradingStrategies[tradingEngine.current.strategy.index.value]
}
function closePosition() {
/* We calculate the new balances after the position is closed. */
let feePaid = 0
if (bot.sessionAndMarketBaseAssetsAreEqual) {
feePaid = tradingEngine.current.balance.quotedAsset.value / tradingEngine.current.position.endRate.value * bot.SESSION.parameters.feeStructure.config.taker / 100
tradingEngine.current.balance.baseAsset.value = tradingEngine.current.balance.baseAsset.value + tradingEngine.current.balance.quotedAsset.value / tradingEngine.current.position.endRate.value - feePaid
tradingEngine.current.balance.quotedAsset.value = 0
} else {
feePaid = tradingEngine.current.balance.baseAsset.value * tradingEngine.current.position.endRate.value * bot.SESSION.parameters.feeStructure.config.taker / 100
tradingEngine.current.balance.quotedAsset.value = tradingEngine.current.balance.quotedAsset.value + tradingEngine.current.balance.baseAsset.value * tradingEngine.current.position.endRate.value - feePaid
tradingEngine.current.balance.baseAsset.value = 0
}
tradingPositionModule.updateStatistics()
/* Recalculating Episode Counters */
if (tradingEngine.current.position.positionStatistics.profitLoss.value > 0) {
tradingEngine.episode.episodeCounters.hits.value++
} else {
tradingEngine.episode.episodeCounters.fails.value++
}
/* Recalculating Episode Statistics */
if (bot.sessionAndMarketBaseAssetsAreEqual) {
tradingEngine.episode.episodeStatistics.profitLoss.value = tradingEngine.current.balance.baseAsset.value - sessionParameters.sessionBaseAsset.config.initialBalance
tradingEngine.episode.episodeStatistics.ROI.value = (sessionParameters.sessionBaseAsset.config.initialBalance + tradingEngine.episode.episodeStatistics.profitLoss.value) / sessionParameters.sessionBaseAsset.config.initialBalance - 1
tradingEngine.episode.episodeStatistics.hitRatio.value = tradingEngine.episode.episodeCounters.hits.value / tradingEngine.episode.episodeCounters.positions.value
tradingEngine.episode.episodeStatistics.anualizedRateOfReturn.value = tradingEngine.episode.episodeStatistics.ROI.value / tradingEngine.episode.episodeStatistics.days.value * 365
} else {
tradingEngine.episode.episodeStatistics.profitLoss.value = tradingEngine.current.balance.quotedAsset.value - sessionParameters.sessionQuotedAsset.config.initialBalance
tradingEngine.episode.episodeStatistics.ROI.value = (sessionParameters.sessionQuotedAsset.config.initialBalance + tradingEngine.episode.episodeStatistics.profitLoss.value) / sessionParameters.sessionQuotedAsset.config.initialBalance - 1
tradingEngine.episode.episodeStatistics.hitRatio.value = tradingEngine.episode.episodeCounters.hits.value / tradingEngine.episode.episodeCounters.positions.value
tradingEngine.episode.episodeStatistics.anualizedRateOfReturn.value = tradingEngine.episode.episodeStatistics.ROI.value / tradingEngine.episode.episodeStatistics.days.value * 365
}
}
function exitStrategyAfterPosition() {
if (tradingEngine.current.strategy.stageType.value !== 'Close Stage') { return }
tradingStrategyModule.closeStrategy('Position Closed')
tradingEngine.current.distanceToEvent.triggerOff.value = 1
if (bot.SESSION.type === 'Backtesting Session') {
if (sessionParameters.snapshots !== undefined) {
if (sessionParameters.snapshots.config.strategy === true) {
snapshotsModule.strategyExit()
}
}
}
if (bot.SESSION.type === 'Backtesting Session') {
if (sessionParameters.snapshots !== undefined) {
if (sessionParameters.snapshots.config.position === true) {
snapshotsModule.positionExit()
}
}
}
}
function getPositionSize() {
let balance
if (bot.sessionAndMarketBaseAssetsAreEqual) {
balance = tradingEngine.current.balance.baseAsset.value
} else {
balance = tradingEngine.current.balance.quotedAsset.value
}
const DEFAULT_VALUE = balance
let strategy = tradingSystem.tradingStrategies[tradingEngine.current.strategy.index.value]
if (strategy.openStage === undefined) return DEFAULT_VALUE
if (strategy.openStage.initialDefinition === undefined) return DEFAULT_VALUE
if (strategy.openStage.initialDefinition.positionSize === undefined) return DEFAULT_VALUE
if (strategy.openStage.initialDefinition.positionSize.formula === undefined) return DEFAULT_VALUE
let value = formulas.get(strategy.openStage.initialDefinition.positionSize.formula.id)
if (value === undefined) return DEFAULT_VALUE
return value
}
function getPositionRate() {
const DEFAULT_VALUE = tradingEngine.current.candle.close.value
let strategy = tradingSystem.tradingStrategies[tradingEngine.current.strategy.index.value]
if (strategy.openStage === undefined) return DEFAULT_VALUE
if (strategy.openStage.initialDefinition === undefined) return DEFAULT_VALUE
if (strategy.openStage.initialDefinition.positionRate === undefined) return DEFAULT_VALUE
if (strategy.openStage.initialDefinition.positionRate.formula === undefined) return DEFAULT_VALUE
let value = formulas.get(strategy.openStage.initialDefinition.positionRate.formula.id)
if (value === undefined) return DEFAULT_VALUE
return value
}
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
}
}
}