2504 lines
144 KiB
JavaScript
2504 lines
144 KiB
JavaScript
exports.newTradingSimulation = function newTradingSimulation(bot, logger, UTILITIES) {
|
|
const FULL_LOG = true
|
|
const LOG_FILE_CONTENT = false
|
|
const ONE_DAY_IN_MILISECONDS = 24 * 60 * 60 * 1000
|
|
const MODULE_NAME = 'Trading Simulation -> ' + bot.SESSION.name
|
|
|
|
const GMT_SECONDS = ':00.000 GMT+0000'
|
|
|
|
let thisObject = {
|
|
finalize: finalize,
|
|
runSimulation: runSimulation
|
|
}
|
|
|
|
let utilities = UTILITIES.newCloudUtilities(bot, logger)
|
|
|
|
return thisObject
|
|
|
|
function finalize() {
|
|
thisObject = undefined
|
|
}
|
|
|
|
function runSimulation(
|
|
chart,
|
|
dataDependencies,
|
|
timeFrame,
|
|
timeFrameLabel,
|
|
currentDay,
|
|
variable,
|
|
exchangeAPI,
|
|
recordsArray,
|
|
conditionsArray,
|
|
strategiesArray,
|
|
positionsArray,
|
|
callback,
|
|
callBackFunction) {
|
|
try {
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> Entering function.') }
|
|
let processingDailyFiles
|
|
if (timeFrame > global.dailyFilePeriods[0][0]) {
|
|
processingDailyFiles = false
|
|
} else {
|
|
processingDailyFiles = true
|
|
}
|
|
|
|
/* Snapshots of Trigger On and Take Positions */
|
|
let snapshots = {
|
|
headers: undefined,
|
|
triggerOn: [],
|
|
takePosition: [],
|
|
lastTriggerOn: undefined,
|
|
lastTakePosition: undefined
|
|
}
|
|
|
|
let tradingSystem = bot.TRADING_SYSTEM
|
|
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> bot.VALUES_TO_USE.timeRange.initialDatetime = ' + bot.VALUES_TO_USE.timeRange.initialDatetime) }
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> bot.VALUES_TO_USE.timeRange.finalDatetime = ' + bot.VALUES_TO_USE.timeRange.finalDatetime) }
|
|
|
|
let timerToCloseStage = 0
|
|
|
|
/* Stop Loss Management */
|
|
|
|
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 */
|
|
|
|
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 for this executioin only */
|
|
let takePositionNow = false
|
|
let closePositionNow = false
|
|
|
|
/* In some cases we need to know if we are positioned at the last candle of the calendar day, for that we need these variables. */
|
|
let lastInstantOfTheDay = 0
|
|
if (currentDay) {
|
|
lastInstantOfTheDay = currentDay.valueOf() + ONE_DAY_IN_MILISECONDS - 1
|
|
}
|
|
|
|
if (variable.isInitialized !== true) {
|
|
variable.isInitialized = true
|
|
|
|
variable.episode = {} // An Episode represents each execution of the Simulation
|
|
variable.episode.count = {} // Everything in here means the total count since the begining of the episode.
|
|
variable.episode.stat = {} // Everything in here means the statistics calculated since the begining of the episode.
|
|
|
|
variable.current = {} // Everything in here means that is happening right now.
|
|
variable.current.balance = {} // Balances at the current candle of the Simulation.
|
|
variable.current.distance = {} // Everything in here means the distance, measured in periods to whatever happened in the past, usually events.
|
|
variable.current.distance.toEvent = {} // Distance to Events that already happened.
|
|
variable.current.strategy = {} // Everything in here are variables related to the current strategy being executed.
|
|
variable.current.position = {} // Everything in here are variables related to the current position being executed.
|
|
|
|
variable.current.balance.baseAsset = bot.VALUES_TO_USE.initialBalanceA
|
|
variable.current.balance.quotedAsset = bot.VALUES_TO_USE.initialBalanceB
|
|
variable.current.distance.toEvent.triggerOn = 0
|
|
variable.current.distance.toEvent.triggerOff = 0
|
|
variable.current.distance.toEvent.takePosition = 0
|
|
variable.current.distance.toEvent.closePosition = 0
|
|
|
|
variable.last = {} // Everything in here means that it already happened.
|
|
variable.last.position = {} // Las values of variables at the last position.
|
|
|
|
variable.last.position.profitLoss = 0
|
|
variable.last.position.ROI = 0
|
|
|
|
variable.previous = {} // Everything in here means the the value that was current before a new current value was set.
|
|
variable.previous.balance = {} // Balances values previous to the current ones.
|
|
|
|
variable.previous.balance.baseAsset = 0
|
|
variable.previous.balance.quotedAsset = 0
|
|
|
|
variable.episode.parameters = {} // Parameters give you access to information received by the Simulation as parameters.
|
|
variable.episode.parameters.initial = {} // Everything in here means the status at the begining of the episode received as a parameter of the Sumulation.
|
|
variable.episode.parameters.minimum = {} // Everything in here means the minimum in the whole episode received as a parameter of the Sumulation.
|
|
variable.episode.parameters.maximum = {} // Everything in here means the maximum in the whole episode received as a parameter of the Sumulation.
|
|
variable.episode.parameters.initial.balance = {}
|
|
variable.episode.parameters.minimum.balance = {}
|
|
variable.episode.parameters.maximum.balance = {}
|
|
|
|
variable.episode.parameters.baseAsset = bot.VALUES_TO_USE.baseAsset
|
|
variable.episode.parameters.quotedAsset = bot.VALUES_TO_USE.quotedAsset
|
|
variable.episode.parameters.marketBaseAsset = bot.market.baseAsset
|
|
variable.episode.parameters.marketQuotedAsset = bot.market.quotedAsset
|
|
variable.episode.parameters.timeFrame = timeFrame // The numeric value of the time frame, in milliseconds
|
|
variable.episode.parameters.timeFrameLabel = timeFrameLabel // The alpha-numeric value of the time frame
|
|
|
|
variable.episode.parameters.initial.balance.baseAsset = bot.VALUES_TO_USE.initialBalanceA
|
|
variable.episode.parameters.minimum.balance.baseAsset = bot.VALUES_TO_USE.minimumBalanceA
|
|
variable.episode.parameters.maximum.balance.baseAsset = bot.VALUES_TO_USE.maximumBalanceA
|
|
|
|
variable.episode.parameters.initial.balance.quotedAsset = bot.VALUES_TO_USE.initialBalanceB
|
|
variable.episode.parameters.minimum.balance.quotedAsset = bot.VALUES_TO_USE.minimumBalanceB
|
|
variable.episode.parameters.maximum.balance.quotedAsset = bot.VALUES_TO_USE.maximumBalanceB
|
|
|
|
variable.episode.count.positions = 0
|
|
variable.episode.count.fails = 0
|
|
variable.episode.count.hits = 0
|
|
variable.episode.count.periods = 0
|
|
|
|
variable.episode.stat.profitLoss = 0
|
|
variable.episode.stat.hitRatio = 0
|
|
variable.episode.stat.days = 0
|
|
variable.episode.stat.ROI = 0
|
|
variable.episode.stat.anualizedRateOfReturn = 0
|
|
|
|
inializeCurrentStrategy()
|
|
initializeCurrentPosition()
|
|
|
|
variable.announcements = []
|
|
}
|
|
|
|
function inializeCurrentStrategy() {
|
|
|
|
variable.current.strategy.name = ''
|
|
variable.current.strategy.index = -1
|
|
variable.current.strategy.stage = 'No Stage'
|
|
variable.current.strategy.begin = 0
|
|
variable.current.strategy.end = 0
|
|
variable.current.strategy.status = 0
|
|
variable.current.strategy.number = 0
|
|
variable.current.strategy.beginRate = 0
|
|
variable.current.strategy.endRate = 0
|
|
variable.current.strategy.situationName = ''
|
|
|
|
}
|
|
|
|
function initializeCurrentPosition() {
|
|
|
|
variable.current.position.begin = 0
|
|
variable.current.position.end = 0
|
|
variable.current.position.rate = 0
|
|
variable.current.position.size = 0
|
|
variable.current.position.status = 0
|
|
variable.current.position.exitType = 0
|
|
variable.current.position.beginRate = 0
|
|
variable.current.position.endRate = 0
|
|
variable.current.position.situationName = ''
|
|
|
|
variable.current.position.stat = {}
|
|
variable.current.position.stat.ROI = 0
|
|
variable.current.position.stat.days = 0
|
|
|
|
variable.current.position.count = {}
|
|
variable.current.position.count.periods = 0
|
|
|
|
variable.current.position.stopLoss = {}
|
|
variable.current.position.stopLoss.value = 0
|
|
variable.current.position.stopLoss.phase = -1
|
|
variable.current.position.stopLoss.stage = 'No Stage'
|
|
|
|
variable.current.position.takeProfit = {}
|
|
variable.current.position.takeProfit.value = 0
|
|
variable.current.position.takeProfit.phase = -1
|
|
variable.current.position.takeProfit.stage = 'No Stage'
|
|
|
|
}
|
|
|
|
/* Main Array and Maps */
|
|
let propertyName = 'at' + variable.episode.parameters.timeFrameLabel.replace('-', '')
|
|
let candles = chart[propertyName].candles
|
|
let currentChart = chart[propertyName]
|
|
|
|
/* Last Candle */
|
|
let lastCandle = candles[candles.length - 1]
|
|
|
|
/* Main Simulation Loop: We go thourgh all the candles at this time period. */
|
|
let currentCandleIndex
|
|
|
|
/* For Loop Level heartbeat */
|
|
let loopingDay
|
|
let previousLoopingDay
|
|
|
|
initializeLoop()
|
|
|
|
function initializeLoop() {
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> initializeLoop -> Entering function.') }
|
|
|
|
/* Estimate Initial Candle */
|
|
|
|
let firstEnd = candles[0].end
|
|
let targetEnd = bot.VALUES_TO_USE.timeRange.initialDatetime.valueOf()
|
|
let diff = targetEnd - firstEnd
|
|
let amount = diff / variable.episode.parameters.timeFrame
|
|
|
|
currentCandleIndex = Math.trunc(amount)
|
|
if (currentCandleIndex < 0) { currentCandleIndex = 0 }
|
|
if (currentCandleIndex > candles.length - 1) {
|
|
/* This will happen when the bot.VALUES_TO_USE.timeRange.initialDatetime is beyond the last candle available, meaning that the dataSet needs to be updated with more up-to-date data. */
|
|
currentCandleIndex = candles.length - 1
|
|
}
|
|
|
|
loop()
|
|
}
|
|
|
|
function loop() {
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> Entering function.') }
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> Processing candle # ' + currentCandleIndex) }
|
|
|
|
/* We are going to take snapshots of the values of indicators at key moments of the sumulation. */
|
|
|
|
let snapshotKeys = new Map()
|
|
snapshotLoopHeaders = []
|
|
snapshotDataRecord = []
|
|
saveAsLastTriggerOnSnapshot = false
|
|
saveAsLastTakePositionSnapshot = false
|
|
addToSnapshots = false
|
|
|
|
let announcementsToBeMade = []
|
|
let candle = candles[currentCandleIndex]
|
|
|
|
/* Not processing while out of user-defined time range */
|
|
if (candle.end < bot.VALUES_TO_USE.timeRange.initialDatetime.valueOf()) {
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> Skipping Candle before the bot.VALUES_TO_USE.timeRange.initialDatetime.') }
|
|
controlLoop()
|
|
return
|
|
}
|
|
if (candle.begin > bot.VALUES_TO_USE.timeRange.finalDatetime.valueOf()) {
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> Skipping Candle after the bot.VALUES_TO_USE.timeRange.finalDatetime.') }
|
|
afterLoop()
|
|
return
|
|
}
|
|
|
|
/* Here we update at the chart data structure the objects for each product representing where we are currently standing at the simulation loop */
|
|
if (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.code.codeName !== 'Multi-Period-Daily') { continue }
|
|
let singularVariableName = dataDependencyNode.referenceParent.parentNode.code.singularVariableName
|
|
let pluralVariableName = dataDependencyNode.referenceParent.parentNode.code.pluralVariableName
|
|
let currentElement = getElement(thisChart[pluralVariableName], candle, '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.code.codeName !== 'Multi-Period-Market') { continue }
|
|
let singularVariableName = dataDependencyNode.referenceParent.parentNode.code.singularVariableName
|
|
let pluralVariableName = dataDependencyNode.referenceParent.parentNode.code.pluralVariableName
|
|
let currentElement = getElement(thisChart[pluralVariableName], candle, 'Market' + '-' + mapKey + '-' + pluralVariableName)
|
|
thisChart[singularVariableName] = currentElement
|
|
}
|
|
}
|
|
|
|
/* Finding the Current Element on Single Files */
|
|
function isItInside(elementWithTimestamp, elementWithBeginEnd) {
|
|
if (elementWithTimestamp.timestamp >= elementWithBeginEnd.begin && elementWithTimestamp.timestamp <= elementWithBeginEnd.end) {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
let propertyName = 'atAnyTimeFrame'
|
|
let thisChart = chart[propertyName]
|
|
|
|
for (let k = 0; k < dataDependencies.length; k++) {
|
|
let dataDependencyNode = dataDependencies[k]
|
|
if (dataDependencyNode.referenceParent.code.codeName !== 'Single-File') { continue }
|
|
let singularVariableName = dataDependencyNode.referenceParent.parentNode.code.singularVariableName
|
|
let pluralVariableName = dataDependencyNode.referenceParent.parentNode.code.pluralVariableName
|
|
let elementArray = thisChart[pluralVariableName]
|
|
let currentElement
|
|
if (elementArray !== undefined) {
|
|
currentElement = elementArray[elementArray.length - 1]
|
|
}
|
|
thisChart[singularVariableName] = currentElement
|
|
}
|
|
|
|
/* While we are processing the previous day. */
|
|
let positionedAtYesterday = false
|
|
if (currentDay) {
|
|
positionedAtYesterday = (candle.end < currentDay.valueOf())
|
|
}
|
|
|
|
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()) }
|
|
|
|
let ticker = {
|
|
bid: candle.close,
|
|
ask: candle.close,
|
|
last: candle.close
|
|
}
|
|
|
|
/* We will produce a simulation level heartbeat in order to inform the user this is running. */
|
|
loopingDay = new Date(Math.trunc(candle.begin / ONE_DAY_IN_MILISECONDS) * ONE_DAY_IN_MILISECONDS)
|
|
if (loopingDay.valueOf() !== previousLoopingDay) {
|
|
let processingDate = loopingDay.getUTCFullYear() + '-' + utilities.pad(loopingDay.getUTCMonth() + 1, 2) + '-' + utilities.pad(loopingDay.getUTCDate(), 2)
|
|
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> Simulation ' + bot.sessionKey + ' Loop # ' + currentCandleIndex + ' @ ' + processingDate) }
|
|
|
|
/* Telling the world we are alive and doing well */
|
|
let fromDate = new Date(bot.VALUES_TO_USE.timeRange.initialDatetime.valueOf())
|
|
let lastDate = new Date(bot.VALUES_TO_USE.timeRange.finalDatetime.valueOf())
|
|
|
|
let currentDateString = loopingDay.getUTCFullYear() + '-' + utilities.pad(loopingDay.getUTCMonth() + 1, 2) + '-' + utilities.pad(loopingDay.getUTCDate(), 2)
|
|
let currentDate = new Date(loopingDay)
|
|
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)
|
|
}
|
|
}
|
|
previousLoopingDay = loopingDay.valueOf()
|
|
|
|
variable.episode.count.periods++
|
|
variable.episode.stat.days = variable.episode.count.periods * variable.episode.parameters.timeFrame / ONE_DAY_IN_MILISECONDS
|
|
|
|
if (processingDailyFiles) {
|
|
/* We skip the candle at the head of the market because currentCandleIndex has not closed yet. */
|
|
let candlesPerDay = ONE_DAY_IN_MILISECONDS / variable.episode.parameters.timeFrame
|
|
if (currentCandleIndex === candles.length - 1) {
|
|
if ((candles.length < candlesPerDay) || (candles.length > candlesPerDay && candles.length < candlesPerDay * 2)) {
|
|
/* We are at the head of the market, thus we skip the last candle because it has not close yet. */
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> Skipping Candle because it is the last one and has not been closed yet.') }
|
|
controlLoop()
|
|
return
|
|
/* 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 variable.episode.count.periods, then there will be a hole in the file since the last period will be missing. */
|
|
}
|
|
}
|
|
} else { // We are processing Market Files
|
|
if (currentCandleIndex === candles.length - 1) {
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> Skipping Candle because it is the last one and has not been closed yet.') }
|
|
controlLoop()
|
|
return
|
|
}
|
|
}
|
|
|
|
let conditions = new Map() // Here we store the conditions values that will be use in the simulator for decision making.
|
|
let formulas = new Map()
|
|
let conditionsValues = [] // Here we store the conditions values that will be written on file for the plotter.
|
|
let formulasErrors = [] // Here we store the errors produced by all phase formulas.
|
|
let formulasValues = [] // Here we store the values produced by all phase formulas.
|
|
|
|
/* We define and evaluate all conditions to be used later during the simulation loop. */
|
|
for (let j = 0; j < tradingSystem.strategies.length; j++) {
|
|
let strategy = tradingSystem.strategies[j]
|
|
|
|
let positionSize = 0
|
|
let positionRate = 0
|
|
|
|
/* Continue with the rest of the formulas and conditions */
|
|
|
|
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]
|
|
|
|
for (let m = 0; m < situation.conditions.length; m++) {
|
|
let condition = situation.conditions[m]
|
|
let key = j + '-' + 'triggerStage' + '-' + 'triggerOn' + '-' + k + '-' + m
|
|
|
|
if (condition.javascriptCode !== undefined) {
|
|
newCondition(key, condition.javascriptCode, chart)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (triggerStage.triggerOff !== undefined) {
|
|
for (let k = 0; k < triggerStage.triggerOff.situations.length; k++) {
|
|
let situation = triggerStage.triggerOff.situations[k]
|
|
|
|
for (let m = 0; m < situation.conditions.length; m++) {
|
|
let condition = situation.conditions[m]
|
|
let key = j + '-' + 'triggerStage' + '-' + 'triggerOff' + '-' + k + '-' + m
|
|
|
|
if (condition.javascriptCode !== undefined) {
|
|
newCondition(key, condition.javascriptCode, chart)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (triggerStage.takePosition !== undefined) {
|
|
for (let k = 0; k < triggerStage.takePosition.situations.length; k++) {
|
|
let situation = triggerStage.takePosition.situations[k]
|
|
|
|
for (let m = 0; m < situation.conditions.length; m++) {
|
|
let condition = situation.conditions[m]
|
|
let key = j + '-' + 'triggerStage' + '-' + 'takePosition' + '-' + k + '-' + m
|
|
|
|
if (condition.javascriptCode !== undefined) {
|
|
newCondition(key, condition.javascriptCode, chart)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let openStage = strategy.openStage
|
|
|
|
if (openStage !== undefined) {
|
|
/* Default Values */
|
|
if (variable.episode.parameters.baseAsset === variable.episode.parameters.marketBaseAsset) {
|
|
positionSize = variable.current.balance.baseAsset
|
|
positionRate = candle.close
|
|
} else {
|
|
positionSize = variable.current.balance.quotedAsset
|
|
positionRate = candle.close
|
|
}
|
|
|
|
let initialDefinition = openStage.initialDefinition
|
|
|
|
if (initialDefinition !== undefined) {
|
|
if (variable.current.position.size !== 0) {
|
|
positionSize = variable.current.position.size
|
|
} else {
|
|
if (initialDefinition.positionSize !== undefined) {
|
|
if (initialDefinition.positionSize.formula !== undefined) {
|
|
try {
|
|
let code = initialDefinition.positionSize.formula.code
|
|
positionSize = eval(code)
|
|
addCodeToSnapshot(code)
|
|
} catch (err) {
|
|
initialDefinition.positionSize.formula.error = err.message
|
|
}
|
|
if (isNaN(positionSize)) {
|
|
if (variable.episode.parameters.baseAsset === variable.episode.parameters.marketBaseAsset) {
|
|
positionSize = variable.current.balance.baseAsset
|
|
} else {
|
|
positionSize = variable.current.balance.quotedAsset
|
|
}
|
|
} else {
|
|
if (variable.episode.parameters.baseAsset === variable.episode.parameters.marketBaseAsset) {
|
|
if (positionSize > variable.current.balance.baseAsset) { positionSize = variable.current.balance.baseAsset }
|
|
} else {
|
|
if (positionSize > variable.current.balance.quotedAsset) { positionSize = variable.current.balance.quotedAsset }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (variable.current.position.rate !== 0) {
|
|
positionRate = variable.current.position.rate
|
|
} else {
|
|
if (initialDefinition.positionRate !== undefined) {
|
|
if (initialDefinition.positionRate.formula !== undefined) {
|
|
try {
|
|
let code = initialDefinition.positionRate.formula.code
|
|
positionRate = eval(initialDefinition.positionRate.formula.code)
|
|
addCodeToSnapshot(code)
|
|
} catch (err) {
|
|
initialDefinition.positionRate.formula.error = err.message
|
|
}
|
|
if (isNaN(positionRate)) {
|
|
if (variable.episode.parameters.baseAsset === variable.episode.parameters.marketBaseAsset) {
|
|
positionRate = candle.close
|
|
} else {
|
|
positionRate = candle.close
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (initialDefinition.stopLoss !== undefined) {
|
|
for (let p = 0; p < initialDefinition.stopLoss.phases.length; p++) {
|
|
let phase = initialDefinition.stopLoss.phases[p]
|
|
|
|
/* Evaluate Formula */
|
|
let formulaValue
|
|
let formulaError = ''
|
|
|
|
if (phase.formula !== undefined) {
|
|
try {
|
|
let code = phase.formula.code
|
|
formulaValue = eval(code)
|
|
addCodeToSnapshot(code)
|
|
if (formulaValue === Infinity) {
|
|
formulaError = 'Formula evaluates to Infinity.'
|
|
formulaValue = MAX_STOP_LOSS_VALUE
|
|
if (variable.current.position.stopLoss.stage === 'Open Stage') {
|
|
formulaError = 'WARNING: Formula evaluates to Infinity.'
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[WARN] runSimulation -> loop -> initialDefinition.stopLoss -> MAX_STOP_LOSS_VALUE -> formulaError = ' + formulaError) }
|
|
}
|
|
}
|
|
} catch (err) {
|
|
if (phase.formula.code.indexOf('previous') > 0 && err.message.indexOf('of undefined') > 0) {
|
|
/*
|
|
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 {
|
|
formulaError = err.message
|
|
}
|
|
}
|
|
if (isNaN(formulaValue)) { formulaValue = 0 }
|
|
if (formulaValue < MIN_STOP_LOSS_VALUE) {
|
|
formulaValue = MIN_STOP_LOSS_VALUE
|
|
if (variable.current.position.stopLoss.stage === 'Open Stage') {
|
|
formulaError = 'WARNING: Formula is evaluating below the MIN_STOP_LOSS_VALUE.'
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[WARN] runSimulation -> loop -> initialDefinition.stopLoss -> MIN_STOP_LOSS_VALUE -> formulaError = ' + formulaError) }
|
|
}
|
|
}
|
|
|
|
formulasErrors.push('"' + formulaError + '"')
|
|
formulasValues.push(formulaValue)
|
|
let key = j + '-' + 'openStage' + '-' + 'initialDefinition' + '-' + 'stopLoss' + '-' + p
|
|
formulas.set(key, formulaValue)
|
|
}
|
|
|
|
/* next phase event */
|
|
let nextPhaseEvent = phase.nextPhaseEvent
|
|
if (nextPhaseEvent !== undefined) {
|
|
|
|
for (let k = 0; k < nextPhaseEvent.situations.length; k++) {
|
|
|
|
let situation = nextPhaseEvent.situations[k]
|
|
|
|
for (let m = 0; m < situation.conditions.length; m++) {
|
|
|
|
let condition = situation.conditions[m]
|
|
let key = j + '-' + 'openStage' + '-' + 'initialDefinition' + '-' + 'stopLoss' + '-' + p + '-' + k + '-' + m
|
|
|
|
if (condition.javascriptCode !== undefined) {
|
|
newCondition(key, condition.javascriptCode, chart)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* 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]
|
|
|
|
for (let m = 0; m < situation.conditions.length; m++) {
|
|
|
|
let condition = situation.conditions[m]
|
|
let key = j + '-' + 'openStage' + '-' + 'initialDefinition' + '-' + 'stopLoss' + '-' + p + '-' + n + '-' + k + '-' + m
|
|
|
|
if (condition.javascriptCode !== undefined) {
|
|
newCondition(key, condition.javascriptCode, chart)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (initialDefinition.takeProfit !== undefined) {
|
|
for (let p = 0; p < initialDefinition.takeProfit.phases.length; p++) {
|
|
let phase = initialDefinition.takeProfit.phases[p]
|
|
|
|
/* Evaluate Formula */
|
|
let formulaValue
|
|
let formulaError = ''
|
|
|
|
if (phase.formula !== undefined) {
|
|
try {
|
|
let code = phase.formula.code
|
|
formulaValue = eval(code)
|
|
addCodeToSnapshot(code)
|
|
if (formulaValue === Infinity) {
|
|
formulaValue = MAX_TAKE_PROFIT_VALUE
|
|
if (variable.current.position.takeProfit.stage === 'Open Stage') {
|
|
formulaError = 'WARNING: Formula evaluates to Infinity.'
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[WARN] runSimulation -> loop -> initialDefinition.takeProfit -> MAX_TAKE_PROFIT_VALUE -> formulaError = ' + formulaError) }
|
|
}
|
|
}
|
|
} catch (err) {
|
|
if (phase.formula.code.indexOf('previous') > 0 && err.message.indexOf('of undefined') > 0) {
|
|
/*
|
|
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 {
|
|
formulaError = err.message
|
|
}
|
|
}
|
|
if (isNaN(formulaValue)) { formulaValue = 0 }
|
|
if (formulaValue < MIN_TAKE_PROFIT_VALUE) {
|
|
formulaValue = MIN_TAKE_PROFIT_VALUE
|
|
if (variable.current.position.takeProfit.stage === 'Open Stage') {
|
|
formulaError = 'WARNING: Formula is evaluating below the MIN_TAKE_PROFIT_VALUE.'
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[WARN] runSimulation -> loop -> initialDefinition.takeProfit -> MIN_TAKE_PROFIT_VALUE -> formulaError = ' + formulaError) }
|
|
}
|
|
}
|
|
|
|
formulasErrors.push('"' + formulaError + '"')
|
|
formulasValues.push(formulaValue)
|
|
let key = j + '-' + 'openStage' + '-' + 'initialDefinition' + '-' + 'takeProfit' + '-' + p
|
|
formulas.set(key, formulaValue)
|
|
}
|
|
|
|
/* next phase event */
|
|
let nextPhaseEvent = phase.nextPhaseEvent
|
|
if (nextPhaseEvent !== undefined) {
|
|
|
|
for (let k = 0; k < nextPhaseEvent.situations.length; k++) {
|
|
|
|
let situation = nextPhaseEvent.situations[k]
|
|
|
|
for (let m = 0; m < situation.conditions.length; m++) {
|
|
|
|
let condition = situation.conditions[m]
|
|
let key = j + '-' + 'openStage' + '-' + 'initialDefinition' + '-' + 'takeProfit' + '-' + p + '-' + k + '-' + m
|
|
|
|
if (condition.javascriptCode !== undefined) {
|
|
newCondition(key, condition.javascriptCode, chart)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* 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]
|
|
|
|
for (let m = 0; m < situation.conditions.length; m++) {
|
|
|
|
let condition = situation.conditions[m]
|
|
let key = j + '-' + 'openStage' + '-' + 'initialDefinition' + '-' + 'takeProfit' + '-' + p + '-' + n + '-' + k + '-' + m
|
|
|
|
if (condition.javascriptCode !== undefined) {
|
|
newCondition(key, condition.javascriptCode, chart)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
strategy.positionSize = positionSize
|
|
strategy.positionRate = positionRate
|
|
}
|
|
|
|
let manageStage = strategy.manageStage
|
|
|
|
if (manageStage !== undefined) {
|
|
if (manageStage.stopLoss !== undefined) {
|
|
for (let p = 0; p < manageStage.stopLoss.phases.length; p++) {
|
|
let phase = manageStage.stopLoss.phases[p]
|
|
|
|
/* Evaluate Formula */
|
|
let formulaValue
|
|
let formulaError = ''
|
|
|
|
if (phase.formula !== undefined) {
|
|
try {
|
|
let code = phase.formula.code
|
|
formulaValue = eval(code)
|
|
addCodeToSnapshot(code)
|
|
if (formulaValue === Infinity) {
|
|
formulaError = ''
|
|
formulaValue = MAX_STOP_LOSS_VALUE
|
|
if (variable.current.position.stopLoss.stage === 'Manage Stage') {
|
|
formulaError = 'WARNING: Formula evaluates to Infinity.'
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[WARN] runSimulation -> loop -> manageStage.stopLoss -> MAX_STOP_LOSS_VALUE -> formulaError = ' + formulaError) }
|
|
}
|
|
}
|
|
} catch (err) {
|
|
if (phase.formula.code.indexOf('previous') > 0 && err.message.indexOf('of undefined') > 0) {
|
|
/*
|
|
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 {
|
|
formulaError = err.message
|
|
}
|
|
}
|
|
if (isNaN(formulaValue)) { formulaValue = 0 }
|
|
if (formulaValue < MIN_STOP_LOSS_VALUE) {
|
|
formulaValue = MIN_STOP_LOSS_VALUE
|
|
if (variable.current.position.stopLoss.stage === 'Manage Stage') {
|
|
formulaError = 'WARNING: Formula is evaluating below the MIN_STOP_LOSS_VALUE.'
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[WARN] runSimulation -> loop -> manageStage.stopLoss -> MIN_STOP_LOSS_VALUE -> formulaError = ' + formulaError) }
|
|
}
|
|
}
|
|
|
|
formulasErrors.push('"' + formulaError + '"')
|
|
formulasValues.push(formulaValue)
|
|
let key = j + '-' + 'manageStage' + '-' + 'stopLoss' + '-' + p
|
|
formulas.set(key, formulaValue)
|
|
}
|
|
|
|
/* next phase event */
|
|
let nextPhaseEvent = phase.nextPhaseEvent
|
|
if (nextPhaseEvent !== undefined) {
|
|
for (let k = 0; k < nextPhaseEvent.situations.length; k++) {
|
|
|
|
let situation = nextPhaseEvent.situations[k]
|
|
|
|
for (let m = 0; m < situation.conditions.length; m++) {
|
|
|
|
let condition = situation.conditions[m]
|
|
let key = j + '-' + 'manageStage' + '-' + 'stopLoss' + '-' + p + '-' + k + '-' + m
|
|
|
|
if (condition.javascriptCode !== undefined) {
|
|
newCondition(key, condition.javascriptCode, chart)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* 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]
|
|
|
|
for (let m = 0; m < situation.conditions.length; m++) {
|
|
|
|
let condition = situation.conditions[m]
|
|
let key = j + '-' + 'manageStage' + '-' + 'stopLoss' + '-' + p + '-' + n + '-' + k + '-' + m
|
|
|
|
if (condition.javascriptCode !== undefined) {
|
|
newCondition(key, condition.javascriptCode, chart)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (manageStage.takeProfit !== undefined) {
|
|
for (let p = 0; p < manageStage.takeProfit.phases.length; p++) {
|
|
let phase = manageStage.takeProfit.phases[p]
|
|
|
|
/* Evaluate Formula */
|
|
let formulaValue
|
|
let formulaError = ''
|
|
|
|
if (phase.formula !== undefined) {
|
|
try {
|
|
let code = phase.formula.code
|
|
formulaValue = eval(code)
|
|
addCodeToSnapshot(code)
|
|
if (formulaValue === Infinity) {
|
|
formulaError = 'Formula evaluates to Infinity.'
|
|
formulaValue = MAX_TAKE_PROFIT_VALUE
|
|
if (variable.current.position.takeProfit.stage === 'Manage Stage') {
|
|
formulaError = 'WARNING: Formula evaluates to Infinity.'
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[WARN] runSimulation -> loop -> manageStage.takeProfit -> MAX_TAKE_PROFIT_VALUE -> formulaError = ' + formulaError) }
|
|
}
|
|
}
|
|
} catch (err) {
|
|
if (phase.formula.code.indexOf('previous') > 0 && err.message.indexOf('of undefined') > 0) {
|
|
/*
|
|
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 {
|
|
formulaError = err.message
|
|
}
|
|
}
|
|
if (isNaN(formulaValue)) { formulaValue = 0 }
|
|
if (formulaValue < MIN_TAKE_PROFIT_VALUE) {
|
|
formulaValue = MIN_TAKE_PROFIT_VALUE
|
|
if (variable.current.position.takeProfit.stage === 'Manage Stage') {
|
|
formulaError = 'WARNING: Formula is evaluating below the MIN_TAKE_PROFIT_VALUE.'
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[WARN] runSimulation -> loop -> manageStage.takeProfit -> MIN_TAKE_PROFIT_VALUE -> formulaError = ' + formulaError) }
|
|
}
|
|
}
|
|
|
|
formulasErrors.push('"' + formulaError + '"')
|
|
formulasValues.push(formulaValue)
|
|
let key = j + '-' + 'manageStage' + '-' + 'takeProfit' + '-' + p
|
|
formulas.set(key, formulaValue)
|
|
}
|
|
|
|
/* next phase event */
|
|
let nextPhaseEvent = phase.nextPhaseEvent
|
|
if (nextPhaseEvent !== undefined) {
|
|
for (let k = 0; k < nextPhaseEvent.situations.length; k++) {
|
|
|
|
let situation = nextPhaseEvent.situations[k]
|
|
|
|
for (let m = 0; m < situation.conditions.length; m++) {
|
|
|
|
let condition = situation.conditions[m]
|
|
let key = j + '-' + 'manageStage' + '-' + 'takeProfit' + '-' + p + '-' + k + '-' + m
|
|
|
|
if (condition.javascriptCode !== undefined) {
|
|
newCondition(key, condition.javascriptCode, chart)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* 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]
|
|
|
|
for (let m = 0; m < situation.conditions.length; m++) {
|
|
|
|
let condition = situation.conditions[m]
|
|
let key = j + '-' + 'manageStage' + '-' + 'takeProfit' + '-' + p + '-' + n + '-' + k + '-' + m
|
|
|
|
if (condition.javascriptCode !== undefined) {
|
|
newCondition(key, condition.javascriptCode, chart)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function newCondition(key, node, chart) {
|
|
let condition
|
|
let value
|
|
|
|
try {
|
|
let code = node.code
|
|
value = eval(code)
|
|
addCodeToSnapshot(code)
|
|
} catch (err) {
|
|
/*
|
|
One possible error is that the conditions references a .previous that is undefined. For this
|
|
reason and others, we will simply set the value to false.
|
|
*/
|
|
value = false
|
|
|
|
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 {
|
|
node.error = err.message + ' @ ' + (new Date(candle.begin)).toLocaleString()
|
|
}
|
|
}
|
|
|
|
condition = {
|
|
key: key,
|
|
value: value
|
|
}
|
|
|
|
conditions.set(condition.key, condition)
|
|
|
|
if (condition.value) {
|
|
conditionsValues.push(1)
|
|
} else {
|
|
conditionsValues.push(0)
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Trigger On Conditions */
|
|
if (
|
|
variable.current.strategy.stage === 'No Stage' &&
|
|
variable.current.strategy.index === -1
|
|
) {
|
|
let minimumBalance
|
|
let maximumBalance
|
|
let balance
|
|
|
|
if (variable.episode.parameters.baseAsset === variable.episode.parameters.marketBaseAsset) {
|
|
balance = variable.current.balance.baseAsset
|
|
minimumBalance = variable.episode.parameters.minimum.balance.baseAsset
|
|
maximumBalance = variable.episode.parameters.maximum.balance.baseAsset
|
|
} else {
|
|
balance = variable.current.balance.quotedAsset
|
|
minimumBalance = variable.episode.parameters.minimum.balance.quotedAsset
|
|
maximumBalance = variable.episode.parameters.maximum.balance.quotedAsset
|
|
}
|
|
|
|
let stopRunningDate = new Date(candle.begin)
|
|
if (balance <= minimumBalance) {
|
|
tradingSystem.error = 'Min Balance @ ' + stopRunningDate.toLocaleString()
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> ' + tradingSystem.error) }
|
|
afterLoop()
|
|
return
|
|
}
|
|
|
|
if (balance >= maximumBalance) {
|
|
tradingSystem.error = 'Max Balance @ ' + stopRunningDate.toLocaleString()
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> ' + tradingSystem.error) }
|
|
afterLoop()
|
|
return
|
|
}
|
|
|
|
/*
|
|
Here we need to pick a strategy, or if there is not suitable strategy for the current
|
|
market conditions, we pass until the next period.
|
|
|
|
To pick a new strategy we will evaluate what we call the trigger on. 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 condition to enter all over again.
|
|
|
|
*/
|
|
|
|
for (let j = 0; j < tradingSystem.strategies.length; j++) {
|
|
if (
|
|
variable.current.strategy.stage !== 'No Stage' ||
|
|
variable.current.strategy.index !== -1
|
|
) { continue }
|
|
|
|
let strategy = tradingSystem.strategies[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 key = j + '-' + 'triggerStage' + '-' + 'triggerOn' + '-' + k + '-' + m
|
|
|
|
let value = false
|
|
if (conditions.get(key) !== undefined) {
|
|
value = conditions.get(key).value
|
|
}
|
|
|
|
if (value === false) { passed = false }
|
|
}
|
|
|
|
if (passed) {
|
|
|
|
variable.current.strategy.stage = 'Trigger Stage'
|
|
checkAnnouncements(triggerStage)
|
|
|
|
variable.current.strategy.index = j
|
|
variable.current.strategy.begin = candle.begin
|
|
variable.current.strategy.beginRate = candle.min
|
|
variable.current.strategy.endRate = candle.min // In case the strategy does not get exited
|
|
variable.current.strategy.situationName = situation.name
|
|
variable.current.strategy.name = strategy.name
|
|
|
|
variable.current.distance.toEvent.triggerOn = 1
|
|
|
|
checkAnnouncements(triggerStage.triggerOn)
|
|
saveAsLastTriggerOnSnapshot = true
|
|
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> Switching to Trigger Stage because conditions at Trigger On Event were met.') }
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Trigger Off Condition */
|
|
if (variable.current.strategy.stage === 'Trigger Stage') {
|
|
let strategy = tradingSystem.strategies[variable.current.strategy.index]
|
|
|
|
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 key = variable.current.strategy.index + '-' + 'triggerStage' + '-' + 'triggerOff' + '-' + k + '-' + m
|
|
|
|
let value = false
|
|
if (conditions.get(key) !== undefined) {
|
|
value = conditions.get(key).value
|
|
}
|
|
|
|
if (value === false) { passed = false }
|
|
}
|
|
|
|
if (passed) {
|
|
variable.current.strategy.number = variable.current.strategy.index
|
|
variable.current.strategy.end = candle.end
|
|
variable.current.strategy.endRate = candle.min
|
|
variable.current.strategy.status = 1 // This means the strategy is closed, i.e. that has a begin and end.
|
|
variable.current.strategy.stage = 'No Stage'
|
|
variable.current.strategy.index = -1
|
|
|
|
variable.current.distance.toEvent.triggerOff = 1
|
|
|
|
checkAnnouncements(triggerStage.triggerOff)
|
|
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> Switching to No Stage because conditions at the Trigger Off Event were met.') }
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Take Position Condition */
|
|
if (variable.current.strategy.stage === 'Trigger Stage') {
|
|
let strategy = tradingSystem.strategies[variable.current.strategy.index]
|
|
|
|
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 key = variable.current.strategy.index + '-' + 'triggerStage' + '-' + 'takePosition' + '-' + k + '-' + m
|
|
|
|
let value = false
|
|
if (conditions.get(key) !== undefined) {
|
|
value = conditions.get(key).value
|
|
}
|
|
|
|
if (value === false) { passed = false }
|
|
}
|
|
|
|
if (passed) {
|
|
variable.current.strategy.stage = 'Open Stage'
|
|
checkAnnouncements(strategy.openStage)
|
|
|
|
variable.current.position.stopLoss.stage = 'Open Stage'
|
|
variable.current.position.takeProfit.stage = 'Open Stage'
|
|
variable.current.position.stopLoss.phase = 0
|
|
variable.current.position.takeProfit.phase = 0
|
|
|
|
takePositionNow = true
|
|
variable.current.position.situationName = situation.name
|
|
|
|
checkAnnouncements(triggerStage.takePosition)
|
|
saveAsLastTakePositionSnapshot = true
|
|
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> Conditions at the Take Position Event were met.') }
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Stop Loss Management */
|
|
if (
|
|
(variable.current.strategy.stage === 'Open Stage' || variable.current.strategy.stage === 'Manage Stage') &&
|
|
takePositionNow !== true
|
|
) {
|
|
checkStopPhases()
|
|
calculateStopLoss()
|
|
}
|
|
|
|
function checkStopPhases() {
|
|
let strategy = tradingSystem.strategies[variable.current.strategy.index]
|
|
|
|
let openStage = strategy.openStage
|
|
let manageStage = strategy.manageStage
|
|
let parentNode
|
|
let j = variable.current.strategy.index
|
|
let stageKey
|
|
let initialDefinitionKey = ''
|
|
let p
|
|
|
|
if (variable.current.position.stopLoss.stage === 'Open Stage' && openStage !== undefined) {
|
|
if (openStage.initialDefinition !== undefined) {
|
|
if (openStage.initialDefinition.stopLoss !== undefined) {
|
|
parentNode = openStage.initialDefinition
|
|
initialDefinitionKey = '-' + 'initialDefinition'
|
|
stageKey = 'openStage'
|
|
p = variable.current.position.stopLoss.phase
|
|
}
|
|
}
|
|
}
|
|
|
|
if (variable.current.position.stopLoss.stage === 'Manage Stage' && manageStage !== undefined) {
|
|
if (manageStage.stopLoss !== undefined) {
|
|
parentNode = manageStage
|
|
stageKey = 'manageStage'
|
|
p = variable.current.position.stopLoss.phase - 1
|
|
}
|
|
}
|
|
|
|
if (parentNode !== undefined) {
|
|
let phase = parentNode.stopLoss.phases[p]
|
|
|
|
/* 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 key = j + '-' + stageKey + initialDefinitionKey + '-' + 'stopLoss' + '-' + p + '-' + k + '-' + m
|
|
|
|
let value = false
|
|
if (conditions.get(key) !== undefined) {
|
|
value = conditions.get(key).value
|
|
}
|
|
|
|
if (value === false) { passed = false }
|
|
}
|
|
|
|
if (passed) {
|
|
variable.current.position.stopLoss.phase++
|
|
variable.current.position.stopLoss.stage = 'Manage Stage'
|
|
if (variable.current.position.takeProfit.phase > 0) {
|
|
variable.current.strategy.stage = 'Manage Stage'
|
|
checkAnnouncements(manageStage, 'Take Profit')
|
|
}
|
|
|
|
checkAnnouncements(nextPhaseEvent)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
/* 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 key = j + '-' + stageKey + initialDefinitionKey + '-' + 'stopLoss' + '-' + p + '-' + n + '-' + k + '-' + m
|
|
|
|
let value = false
|
|
if (conditions.get(key) !== undefined) {
|
|
value = conditions.get(key).value
|
|
}
|
|
|
|
if (value === false) { passed = false }
|
|
}
|
|
|
|
if (passed) {
|
|
|
|
let moveToPhase = moveToPhaseEvent.referenceParent
|
|
if (moveToPhase !== undefined) {
|
|
for (let q = 0; q < parentNode.stopLoss.phases.length; q++) {
|
|
if (parentNode.stopLoss.phases[q].id === moveToPhase.id) {
|
|
variable.current.position.stopLoss.phase = q + 1
|
|
}
|
|
}
|
|
} else {
|
|
moveToPhaseEvent.error = 'This Node needs to reference a Phase.'
|
|
continue
|
|
}
|
|
|
|
variable.current.position.stopLoss.stage = 'Manage Stage'
|
|
if (variable.current.position.takeProfit.phase > 0) {
|
|
variable.current.strategy.stage = 'Manage Stage'
|
|
checkAnnouncements(manageStage, 'Take Profit')
|
|
}
|
|
|
|
checkAnnouncements(moveToPhaseEvent)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function calculateStopLoss() {
|
|
let strategy = tradingSystem.strategies[variable.current.strategy.index]
|
|
let openStage = strategy.openStage
|
|
let manageStage = strategy.manageStage
|
|
let phase
|
|
let key
|
|
|
|
if (variable.current.position.stopLoss.stage === 'Open Stage' && openStage !== undefined) {
|
|
if (openStage.initialDefinition !== undefined) {
|
|
if (openStage.initialDefinition.stopLoss !== undefined) {
|
|
phase = openStage.initialDefinition.stopLoss.phases[variable.current.position.stopLoss.phase]
|
|
key = variable.current.strategy.index + '-' + 'openStage' + '-' + 'initialDefinition' + '-' + 'stopLoss' + '-' + (variable.current.position.stopLoss.phase)
|
|
}
|
|
}
|
|
}
|
|
|
|
if (variable.current.position.stopLoss.stage === 'Manage Stage' && manageStage !== undefined) {
|
|
if (manageStage.stopLoss !== undefined) {
|
|
phase = manageStage.stopLoss.phases[variable.current.position.stopLoss.phase - 1]
|
|
key = variable.current.strategy.index + '-' + 'manageStage' + '-' + 'stopLoss' + '-' + (variable.current.position.stopLoss.phase - 1)
|
|
}
|
|
}
|
|
|
|
if (phase !== undefined) {
|
|
if (phase.formula !== undefined) {
|
|
let previousValue = variable.current.position.stopLoss.value
|
|
|
|
variable.current.position.stopLoss.value = formulas.get(key)
|
|
|
|
if (variable.current.position.stopLoss.value !== previousValue) {
|
|
checkAnnouncements(phase, variable.current.position.stopLoss.value)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Take Profit Management */
|
|
if (
|
|
(variable.current.strategy.stage === 'Open Stage' || variable.current.strategy.stage === 'Manage Stage') &&
|
|
takePositionNow !== true
|
|
) {
|
|
checkTakeProfitPhases()
|
|
calculateTakeProfit()
|
|
}
|
|
|
|
function checkTakeProfitPhases() {
|
|
let strategy = tradingSystem.strategies[variable.current.strategy.index]
|
|
|
|
let openStage = strategy.openStage
|
|
let manageStage = strategy.manageStage
|
|
let parentNode
|
|
let j = variable.current.strategy.index
|
|
let stageKey
|
|
let initialDefinitionKey = ''
|
|
let p
|
|
|
|
if (variable.current.position.takeProfit.stage === 'Open Stage' && openStage !== undefined) {
|
|
if (openStage.initialDefinition !== undefined) {
|
|
if (openStage.initialDefinition.takeProfit !== undefined) {
|
|
parentNode = openStage.initialDefinition
|
|
initialDefinitionKey = '-' + 'initialDefinition'
|
|
stageKey = 'openStage'
|
|
p = variable.current.position.takeProfit.phase
|
|
}
|
|
}
|
|
}
|
|
|
|
if (variable.current.position.takeProfit.stage === 'Manage Stage' && manageStage !== undefined) {
|
|
if (manageStage.takeProfit !== undefined) {
|
|
parentNode = manageStage
|
|
stageKey = 'manageStage'
|
|
p = variable.current.position.takeProfit.phase - 1
|
|
}
|
|
}
|
|
|
|
if (parentNode !== undefined) {
|
|
let phase = parentNode.takeProfit.phases[p]
|
|
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 key = j + '-' + stageKey + initialDefinitionKey + '-' + 'takeProfit' + '-' + p + '-' + k + '-' + m
|
|
|
|
let value = false
|
|
if (conditions.get(key) !== undefined) {
|
|
value = conditions.get(key).value
|
|
}
|
|
|
|
if (value === false) { passed = false }
|
|
}
|
|
|
|
if (passed) {
|
|
variable.current.position.takeProfit.phase++
|
|
variable.current.position.takeProfit.stage = 'Manage Stage'
|
|
if (variable.current.position.stopLoss.phase > 0) {
|
|
variable.current.strategy.stage = 'Manage Stage'
|
|
checkAnnouncements(manageStage, 'Stop')
|
|
}
|
|
|
|
checkAnnouncements(nextPhaseEvent)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
/* 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 key = j + '-' + stageKey + initialDefinitionKey + '-' + 'takeProfit' + '-' + p + '-' + n + '-' + k + '-' + m
|
|
|
|
let value = false
|
|
if (conditions.get(key) !== undefined) {
|
|
value = conditions.get(key).value
|
|
}
|
|
|
|
if (value === false) { passed = false }
|
|
}
|
|
|
|
if (passed) {
|
|
|
|
let moveToPhase = moveToPhaseEvent.referenceParent
|
|
if (moveToPhase !== undefined) {
|
|
for (let q = 0; q < parentNode.takeProfit.phases.length; q++) {
|
|
if (parentNode.takeProfit.phases[q].id === moveToPhase.id) {
|
|
variable.current.position.takeProfit.phase = q + 1
|
|
}
|
|
}
|
|
} else {
|
|
moveToPhaseEvent.error = 'This Node needs to reference a Phase.'
|
|
continue
|
|
}
|
|
|
|
variable.current.position.takeProfit.stage = 'Manage Stage'
|
|
if (variable.current.position.stopLoss.phase > 0) {
|
|
variable.current.strategy.stage = 'Manage Stage'
|
|
checkAnnouncements(manageStage, 'Stop')
|
|
}
|
|
|
|
checkAnnouncements(moveToPhaseEvent)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function calculateTakeProfit() {
|
|
let strategy = tradingSystem.strategies[variable.current.strategy.index]
|
|
let openStage = strategy.openStage
|
|
let manageStage = strategy.manageStage
|
|
let phase
|
|
let key
|
|
|
|
if (variable.current.position.takeProfit.stage === 'Open Stage' && openStage !== undefined) {
|
|
if (openStage.initialDefinition !== undefined) {
|
|
if (openStage.initialDefinition.takeProfit !== undefined) {
|
|
phase = openStage.initialDefinition.takeProfit.phases[variable.current.position.takeProfit.phase]
|
|
key = variable.current.strategy.index + '-' + 'openStage' + '-' + 'initialDefinition' + '-' + 'takeProfit' + '-' + (variable.current.position.takeProfit.phase)
|
|
}
|
|
}
|
|
}
|
|
|
|
if (variable.current.position.takeProfit.stage === 'Manage Stage' && manageStage !== undefined) {
|
|
if (manageStage.takeProfit !== undefined) {
|
|
phase = manageStage.takeProfit.phases[variable.current.position.takeProfit.phase - 1]
|
|
key = variable.current.strategy.index + '-' + 'manageStage' + '-' + 'takeProfit' + '-' + (variable.current.position.takeProfit.phase - 1)
|
|
}
|
|
}
|
|
|
|
if (phase !== undefined) {
|
|
if (phase.formula !== undefined) {
|
|
let previousValue = variable.current.position.stopLoss.value
|
|
|
|
variable.current.position.takeProfit.value = formulas.get(key)
|
|
|
|
if (variable.current.position.takeProfit.value !== previousValue) {
|
|
checkAnnouncements(phase, variable.current.position.takeProfit.value)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Keeping Position Counters Up-to-date */
|
|
if (
|
|
(variable.current.strategy.stage === 'Open Stage' || variable.current.strategy.stage === 'Manage Stage')
|
|
) {
|
|
if (takePositionNow === true) {
|
|
variable.current.position.count.periods = 0
|
|
}
|
|
|
|
variable.current.position.count.periods++
|
|
variable.current.position.stat.days = variable.current.position.count.periods * variable.episode.parameters.timeFrame / ONE_DAY_IN_MILISECONDS
|
|
} else {
|
|
variable.current.position.count.periods = 0
|
|
variable.current.position.stat.days = 0
|
|
}
|
|
|
|
/* Keeping Distance Counters Up-to-date */
|
|
if (
|
|
variable.current.distance.toEvent.triggerOn > 0 // with this we avoind counting before the first event happens.
|
|
) {
|
|
variable.current.distance.toEvent.triggerOn++
|
|
}
|
|
|
|
if (
|
|
variable.current.distance.toEvent.triggerOff > 0 // with this we avoind counting before the first event happens.
|
|
) {
|
|
variable.current.distance.toEvent.triggerOff++
|
|
}
|
|
|
|
if (
|
|
variable.current.distance.toEvent.takePosition > 0 // with this we avoind counting before the first event happens.
|
|
) {
|
|
variable.current.distance.toEvent.takePosition++
|
|
}
|
|
|
|
if (
|
|
variable.current.distance.toEvent.closePosition > 0 // with this we avoind counting before the first event happens.
|
|
) {
|
|
variable.current.distance.toEvent.closePosition++
|
|
}
|
|
|
|
/* Checking if Stop or Take Profit were hit */
|
|
if (
|
|
(variable.current.strategy.stage === 'Open Stage' || variable.current.strategy.stage === 'Manage Stage') &&
|
|
takePositionNow !== true
|
|
) {
|
|
let strategy = tradingSystem.strategies[variable.current.strategy.index]
|
|
|
|
/* 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 ((variable.episode.parameters.baseAsset === variable.episode.parameters.marketBaseAsset && candle.max >= variable.current.position.stopLoss.value) || (variable.episode.parameters.baseAsset !== variable.episode.parameters.marketBaseAsset && candle.min <= variable.current.position.stopLoss.value)) {
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> Stop Loss was hit.') }
|
|
/*
|
|
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 (variable.episode.parameters.baseAsset === variable.episode.parameters.marketBaseAsset) {
|
|
if (variable.current.position.stopLoss.value < candle.min) {
|
|
variable.current.position.stopLoss.value = candle.min
|
|
}
|
|
} else {
|
|
if (variable.current.position.stopLoss.value > candle.max) {
|
|
variable.current.position.stopLoss.value = candle.max
|
|
}
|
|
}
|
|
|
|
let slippedStopLoss = variable.current.position.stopLoss.value
|
|
|
|
/* Apply the Slippage */
|
|
let slippageAmount = slippedStopLoss * bot.VALUES_TO_USE.slippage.stopLoss / 100
|
|
|
|
if (variable.episode.parameters.baseAsset === variable.episode.parameters.marketBaseAsset) {
|
|
slippedStopLoss = slippedStopLoss + slippageAmount
|
|
} else {
|
|
slippedStopLoss = slippedStopLoss - slippageAmount
|
|
}
|
|
|
|
closeRate = slippedStopLoss
|
|
|
|
variable.current.strategy.stage = 'Close Stage'
|
|
checkAnnouncements(strategy.closeStage, 'Stop')
|
|
|
|
variable.current.position.stopLoss.stage = 'No Stage'
|
|
variable.current.position.takeProfit.stage = 'No Stage'
|
|
variable.current.position.end = candle.end
|
|
variable.current.position.status = 1
|
|
variable.current.position.exitType = 1
|
|
variable.current.position.endRate = closeRate
|
|
|
|
closePositionNow = true
|
|
}
|
|
|
|
/* Take Profit condition: Here we verify if the Take Profit was hit or not. */
|
|
|
|
if ((variable.episode.parameters.baseAsset === variable.episode.parameters.marketBaseAsset && candle.min <= variable.current.position.takeProfit.value) || (variable.episode.parameters.baseAsset !== variable.episode.parameters.marketBaseAsset && candle.max >= variable.current.position.takeProfit.value)) {
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> Take Profit was hit.') }
|
|
/*
|
|
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 (variable.episode.parameters.baseAsset === variable.episode.parameters.marketBaseAsset) {
|
|
if (variable.current.position.takeProfit.value > candle.max) {
|
|
variable.current.position.takeProfit.value = candle.max
|
|
}
|
|
} else {
|
|
if (variable.current.position.takeProfit.value < candle.min) {
|
|
variable.current.position.takeProfit.value = candle.min
|
|
}
|
|
}
|
|
|
|
let slippedTakeProfit = variable.current.position.takeProfit.value
|
|
/* Apply the Slippage */
|
|
let slippageAmount = slippedTakeProfit * bot.VALUES_TO_USE.slippage.takeProfit / 100
|
|
|
|
if (variable.episode.parameters.baseAsset === variable.episode.parameters.marketBaseAsset) {
|
|
slippedTakeProfit = slippedTakeProfit + slippageAmount
|
|
} else {
|
|
slippedTakeProfit = slippedTakeProfit - slippageAmount
|
|
}
|
|
|
|
closeRate = slippedTakeProfit
|
|
|
|
variable.current.strategy.stage = 'Close Stage'
|
|
checkAnnouncements(strategy.closeStage, 'Take Profit')
|
|
|
|
variable.current.position.stopLoss.stage = 'No Stage'
|
|
variable.current.position.takeProfit.stage = 'No Stage'
|
|
|
|
variable.current.position.end = candle.end
|
|
variable.current.position.status = 1
|
|
variable.current.position.exitType = 2
|
|
variable.current.position.endRate = closeRate
|
|
|
|
closePositionNow = true
|
|
addToSnapshots = true
|
|
}
|
|
}
|
|
|
|
/* Taking a Position */
|
|
if (
|
|
takePositionNow === true
|
|
) {
|
|
takePositionNow = false
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> takePositionNow -> Entering code block.') }
|
|
|
|
/* Inicializing this counter */
|
|
variable.current.distance.toEvent.takePosition = 1
|
|
|
|
/* Position size and rate */
|
|
let strategy = tradingSystem.strategies[variable.current.strategy.index]
|
|
|
|
variable.current.position.size = strategy.positionSize
|
|
variable.current.position.rate = strategy.positionRate
|
|
|
|
/* We take what was calculated at the formula and apply the slippage. */
|
|
let slippageAmount = variable.current.position.rate * bot.VALUES_TO_USE.slippage.positionRate / 100
|
|
|
|
if (variable.episode.parameters.baseAsset === variable.episode.parameters.marketBaseAsset) {
|
|
variable.current.position.rate = variable.current.position.rate - slippageAmount
|
|
} else {
|
|
variable.current.position.rate = variable.current.position.rate + slippageAmount
|
|
}
|
|
|
|
/* Update the trade record information. */
|
|
variable.current.position.begin = candle.begin
|
|
variable.current.position.beginRate = variable.current.position.rate
|
|
|
|
/* Check if we need to execute. */
|
|
if (currentCandleIndex > candles.length - 10) { /* Only at the last candles makes sense to check if we are in live mode or not. */
|
|
/* Check that we are in LIVE MODE */
|
|
if (bot.startMode === 'Live') {
|
|
/* 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.
|
|
if (FULL_LOG === true) { 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.
|
|
if (FULL_LOG === true) { 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 {
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> takePositionNow -> Not trading live.') }
|
|
}
|
|
} else {
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> takePositionNow -> Not the last closed candle.') }
|
|
}
|
|
|
|
takePositionAtSimulation()
|
|
|
|
return
|
|
|
|
function putOpeningOrder() {
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> putOpeningOrder -> Entering function.') }
|
|
|
|
/* We wont take a position unless we are withing the bot.VALUES_TO_USE.timeRange.initialDatetime and the bot.VALUES_TO_USE.timeRange.finalDatetime range */
|
|
if (bot.VALUES_TO_USE.timeRange.initialDatetime !== undefined) {
|
|
if (candle.end < bot.VALUES_TO_USE.timeRange.initialDatetime.valueOf()) {
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> putOpeningOrder -> Not placing the trade at the exchange because current candle ends before the start date. -> bot.VALUES_TO_USE.timeRange.initialDatetime = ' + bot.VALUES_TO_USE.timeRange.initialDatetime) }
|
|
takePositionAtSimulation()
|
|
return
|
|
}
|
|
}
|
|
|
|
/* We wont take a position if we are past the final datetime */
|
|
if (bot.VALUES_TO_USE.timeRange.finalDatetime !== undefined) {
|
|
if (candle.begin > bot.VALUES_TO_USE.timeRange.finalDatetime.valueOf()) {
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> putOpeningOrder -> Not placing the trade at the exchange because current candle begins after the end date. -> bot.VALUES_TO_USE.timeRange.finalDatetime = ' + bot.VALUES_TO_USE.timeRange.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 (variable.episode.count.periods <= variable.executionContext.periods) {
|
|
if (FULL_LOG === true) { 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()) / ONE_DAY_IN_MILISECONDS) * ONE_DAY_IN_MILISECONDS)
|
|
let processDay = new Date(Math.trunc(currentDay.valueOf() / ONE_DAY_IN_MILISECONDS) * ONE_DAY_IN_MILISECONDS)
|
|
if (today.valueOf() !== processDay.valueOf()) {
|
|
if (FULL_LOG === true) { 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.') }
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> putOpeningOrder -> today = ' + today) }
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> putOpeningOrder -> processDay = ' + processDay) }
|
|
takePositionAtSimulation()
|
|
return
|
|
}
|
|
}
|
|
|
|
let orderPrice
|
|
let amountA
|
|
let amountB
|
|
let orderSide
|
|
|
|
if (variable.episode.parameters.baseAsset === variable.episode.parameters.marketBaseAsset) {
|
|
orderSide = 'sell'
|
|
|
|
orderPrice = variable.current.position.rate
|
|
|
|
amountA = variable.current.position.size * orderPrice
|
|
amountB = variable.current.position.size
|
|
} else {
|
|
orderSide = 'buy'
|
|
|
|
orderPrice = variable.current.position.rate
|
|
|
|
amountA = variable.current.position.size
|
|
amountB = variable.current.position.size / orderPrice
|
|
}
|
|
|
|
variable.executionContext = {
|
|
status: 'Taking Position',
|
|
periods: variable.episode.count.periods
|
|
}
|
|
|
|
if (FULL_LOG === true) { 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) {
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> putOpeningOrder -> onOrderCreated -> Entering function.') }
|
|
|
|
try {
|
|
switch (err.result) {
|
|
case global.DEFAULT_OK_RESPONSE.result: {
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> putOpeningOrder -> onOrderCreated -> DEFAULT_OK_RESPONSE ') }
|
|
variable.executionContext = {
|
|
status: 'In a Position',
|
|
periods: variable.episode.count.periods,
|
|
amountA: amountA,
|
|
amountB: amountB,
|
|
orderId: order.id
|
|
}
|
|
takePositionAtSimulation()
|
|
return
|
|
}
|
|
case global.DEFAULT_FAIL_RESPONSE.result: {
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> putOpeningOrder -> onOrderCreated -> DEFAULT_FAIL_RESPONSE ') }
|
|
if (FULL_LOG === true) { 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: {
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> putOpeningOrder -> onOrderCreated -> DEFAULT_RETRY_RESPONSE ') }
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[ERROR] runSimulation -> loop -> putOpeningOrder -> onOrderCreated -> Message = ' + err.message) }
|
|
strategy.openStage.openExecution.error = err.message
|
|
afterLoop()
|
|
return
|
|
}
|
|
}
|
|
if (FULL_LOG === true) { 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 takePositionAtSimulation() {
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> takePositionAtSimulation -> Entering function.') }
|
|
|
|
/* Continue with the simulation */
|
|
calculateTakeProfit()
|
|
calculateStopLoss()
|
|
|
|
variable.previous.balance.baseAsset = variable.current.balance.baseAsset
|
|
variable.previous.balance.quotedAsset = variable.current.balance.quotedAsset
|
|
|
|
variable.last.position.profitLoss = 0
|
|
variable.last.position.ROI = 0
|
|
|
|
let feePaid = 0
|
|
|
|
if (variable.episode.parameters.baseAsset === variable.episode.parameters.marketBaseAsset) {
|
|
feePaid = variable.current.position.size * variable.current.position.rate * bot.VALUES_TO_USE.feeStructure.taker / 100
|
|
|
|
variable.current.balance.quotedAsset = variable.current.balance.quotedAsset + variable.current.position.size * variable.current.position.rate - feePaid
|
|
variable.current.balance.baseAsset = variable.current.balance.baseAsset - variable.current.position.size
|
|
} else {
|
|
feePaid = variable.current.position.size / variable.current.position.rate * bot.VALUES_TO_USE.feeStructure.taker / 100
|
|
|
|
variable.current.balance.baseAsset = variable.current.balance.baseAsset + variable.current.position.size / variable.current.position.rate - feePaid
|
|
variable.current.balance.quotedAsset = variable.current.balance.quotedAsset - variable.current.position.size
|
|
}
|
|
|
|
addRecords()
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> takePositionAtSimulation -> Exiting Loop Body after taking position at simulation.') }
|
|
controlLoop()
|
|
return
|
|
}
|
|
}
|
|
|
|
if (closePositionNow === true) {
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> Closing a Position -> Entering code block.') }
|
|
|
|
closePositionNow = false
|
|
|
|
/* Inicializing this counter */
|
|
variable.current.distance.toEvent.closePosition = 1
|
|
|
|
/* Position size and rate */
|
|
let strategy = tradingSystem.strategies[variable.current.strategy.index]
|
|
|
|
if (currentCandleIndex > candles.length - 10) { /* Only at the last candles makes sense to check if we are in live mode or not. */
|
|
/* Check that we are in LIVE MODE */
|
|
if (bot.startMode === 'Live') {
|
|
/* 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.
|
|
if (FULL_LOG === true) { 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': { //
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> Closing a Position -> Exiting code block because status is Position Closed.') }
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> Closing a Position -> Not within the last 10 candles.') }
|
|
}
|
|
|
|
closePositionAtSimulation()
|
|
return
|
|
|
|
function putClosingOrder() {
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> putClosingOrder -> Entering function.') }
|
|
|
|
/* 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 (variable.episode.count.periods <= variable.executionContext.periods) {
|
|
if (FULL_LOG === true) { 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 (variable.episode.parameters.baseAsset === variable.episode.parameters.marketBaseAsset) {
|
|
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 = variable.current.balance.quotedAsset
|
|
amountB = variable.current.balance.quotedAsset / 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 = variable.current.balance.baseAsset * orderPrice
|
|
amountB = variable.current.balance.baseAsset
|
|
}
|
|
|
|
variable.executionContext = {
|
|
status: 'Closing Position',
|
|
periods: variable.episode.count.periods
|
|
}
|
|
|
|
if (FULL_LOG === true) { 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) {
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> putClosingOrder -> onOrderCreated -> Entering function.') }
|
|
|
|
try {
|
|
switch (err.result) {
|
|
case global.DEFAULT_OK_RESPONSE.result: {
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> putClosingOrder -> onOrderCreated -> DEFAULT_OK_RESPONSE ') }
|
|
variable.executionContext = {
|
|
status: 'Position Closed',
|
|
periods: variable.episode.count.periods,
|
|
amountA: amountA,
|
|
amountB: amountB,
|
|
orderId: order.id
|
|
}
|
|
closePositionAtSimulation()
|
|
return
|
|
}
|
|
case global.DEFAULT_FAIL_RESPONSE.result: {
|
|
if (FULL_LOG === true) { 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: {
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> putOpeningOrder -> onOrderCreated -> DEFAULT_RETRY_RESPONSE ') }
|
|
strategy.closeStage.closeExecution.error = err.message
|
|
afterLoop()
|
|
return
|
|
}
|
|
}
|
|
if (FULL_LOG === true) { 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
|
|
}
|
|
}
|
|
}
|
|
|
|
function closePositionAtSimulation() {
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> closePositionAtSimulation -> Entering function.') }
|
|
|
|
variable.episode.count.positions++
|
|
|
|
let feePaid = 0
|
|
|
|
if (variable.episode.parameters.baseAsset === variable.episode.parameters.marketBaseAsset) {
|
|
strategy.positionSize = variable.current.balance.quotedAsset / closeRate
|
|
strategy.positionRate = closeRate
|
|
|
|
feePaid = variable.current.balance.quotedAsset / closeRate * bot.VALUES_TO_USE.feeStructure.taker / 100
|
|
|
|
variable.current.balance.baseAsset = variable.current.balance.baseAsset + variable.current.balance.quotedAsset / closeRate - feePaid
|
|
variable.current.balance.quotedAsset = 0
|
|
} else {
|
|
strategy.positionSize = variable.current.balance.baseAsset * closeRate
|
|
strategy.positionRate = closeRate
|
|
|
|
feePaid = variable.current.balance.baseAsset * closeRate * bot.VALUES_TO_USE.feeStructure.taker / 100
|
|
|
|
variable.current.balance.quotedAsset = variable.current.balance.quotedAsset + variable.current.balance.baseAsset * closeRate - feePaid
|
|
variable.current.balance.baseAsset = 0
|
|
}
|
|
|
|
if (variable.episode.parameters.baseAsset === variable.episode.parameters.marketBaseAsset) {
|
|
variable.last.position.profitLoss = variable.current.balance.baseAsset - variable.previous.balance.baseAsset
|
|
variable.last.position.ROI = variable.last.position.profitLoss * 100 / variable.current.position.size
|
|
if (isNaN(variable.last.position.ROI)) { variable.last.position.ROI = 0 }
|
|
variable.episode.stat.profitLoss = variable.current.balance.baseAsset - variable.episode.parameters.initial.balance.baseAsset
|
|
} else {
|
|
variable.last.position.profitLoss = variable.current.balance.quotedAsset - variable.previous.balance.quotedAsset
|
|
variable.last.position.ROI = variable.last.position.profitLoss * 100 / variable.current.position.size
|
|
if (isNaN(variable.last.position.ROI)) { variable.last.position.ROI = 0 }
|
|
variable.episode.stat.profitLoss = variable.current.balance.quotedAsset - variable.episode.parameters.initial.balance.quotedAsset
|
|
}
|
|
|
|
variable.current.position.stat.ROI = variable.last.position.ROI
|
|
|
|
if (variable.last.position.profitLoss > 0) {
|
|
variable.episode.count.hits++
|
|
} else {
|
|
variable.episode.count.fails++
|
|
}
|
|
|
|
if (variable.episode.parameters.baseAsset === variable.episode.parameters.marketBaseAsset) {
|
|
variable.episode.stat.ROI = (variable.episode.parameters.initial.balance.baseAsset + variable.episode.stat.profitLoss) / variable.episode.parameters.initial.balance.baseAsset - 1
|
|
variable.episode.stat.hitRatio = variable.episode.count.hits / variable.episode.count.positions
|
|
variable.episode.stat.anualizedRateOfReturn = variable.episode.stat.ROI / variable.episode.stat.days * 365
|
|
} else {
|
|
variable.episode.stat.ROI = (variable.episode.parameters.initial.balance.quotedAsset + variable.episode.stat.profitLoss) / variable.episode.parameters.initial.balance.quotedAsset - 1
|
|
variable.episode.stat.hitRatio = variable.episode.count.hits / variable.episode.count.positions
|
|
variable.episode.stat.anualizedRateOfReturn = variable.episode.stat.ROI / variable.episode.stat.days * 365
|
|
}
|
|
|
|
addRecords()
|
|
|
|
variable.current.position.stopLoss.value = 0
|
|
variable.current.position.takeProfit.value = 0
|
|
|
|
variable.current.position.rate = 0
|
|
variable.current.position.size = 0
|
|
|
|
timerToCloseStage = candle.begin
|
|
variable.current.position.stopLoss.stage = 'No Stage'
|
|
variable.current.position.takeProfit.stage = 'No Stage'
|
|
variable.current.position.stopLoss.phase = -1
|
|
variable.current.position.takeProfit.phase = -1
|
|
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> closePositionAtSimulation -> Exiting Loop Body after closing position at simulation.') }
|
|
controlLoop()
|
|
return
|
|
}
|
|
}
|
|
|
|
/* Closing the Closing Stage */
|
|
if (variable.current.strategy.stage === 'Close Stage') {
|
|
if (candle.begin - 5 * 60 * 1000 > timerToCloseStage) {
|
|
variable.current.strategy.number = variable.current.strategy.index
|
|
variable.current.strategy.end = candle.end
|
|
variable.current.strategy.endRate = candle.min
|
|
variable.current.strategy.status = 1 // This means the strategy is closed, i.e. that has a begin and end.
|
|
|
|
variable.current.strategy.index = -1
|
|
variable.current.strategy.stage = 'No Stage'
|
|
|
|
timerToCloseStage = 0
|
|
variable.current.distance.toEvent.triggerOff = 1
|
|
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> Closing the Closing Stage -> Exiting Close Stage.') }
|
|
} else {
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> Closing the Closing Stage -> Waiting for timer.') }
|
|
}
|
|
}
|
|
|
|
/* Not a buy or sell condition */
|
|
|
|
addRecords()
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> Exiting Loop Body after adding a record.') }
|
|
controlLoop()
|
|
return
|
|
|
|
function addRecords() {
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> addRecords -> Entering function.') }
|
|
|
|
manageSnapshots()
|
|
addSimulationRecord()
|
|
addConditionsRecord()
|
|
addStrategyRecord()
|
|
addPositionRecord()
|
|
|
|
/* After everything at the simulation level was done, we will do the annoucements that are pending. */
|
|
makeAnnoucements()
|
|
|
|
function manageSnapshots() {
|
|
/* Snapshots Management (before we generate the trade record and delete that info) */
|
|
if (saveAsLastTriggerOnSnapshot === true) {
|
|
snapshots.lastTriggerOn = snapshotDataRecord
|
|
saveAsLastTriggerOnSnapshot = false
|
|
}
|
|
|
|
if (saveAsLastTakePositionSnapshot === true) {
|
|
snapshots.lastTakePosition = snapshotDataRecord
|
|
saveAsLastTakePositionSnapshot = false
|
|
}
|
|
|
|
if (addToSnapshots === true) {
|
|
let closeValues = [
|
|
variable.episode.count.positions, // Position Number
|
|
(new Date(candle.begin)).toISOString(), // Datetime
|
|
tradingSystem.strategies[variable.current.strategy.index].name, // Strategy Name
|
|
variable.current.strategy.situationName, // Trigger On Situation
|
|
variable.current.position.situationName, // Take Position Situation
|
|
hitOrFial(), // Result
|
|
variable.last.position.ROI, // ROI
|
|
exitType() // Exit Type
|
|
]
|
|
|
|
function hitOrFial() {
|
|
if (variable.last.position.ROI > 0) { return 'HIT' } else { return 'FAIL' }
|
|
}
|
|
|
|
function exitType() {
|
|
switch (variable.current.position.exitType) {
|
|
case 1: return 'Stop'
|
|
case 2: return 'Take Profit'
|
|
}
|
|
}
|
|
|
|
|
|
if (positionedAtYesterday === false) {
|
|
snapshots.triggerOn.push(closeValues.concat(snapshots.lastTriggerOn))
|
|
snapshots.takePosition.push(closeValues.concat(snapshots.lastTakePosition))
|
|
}
|
|
snapshots.lastTriggerOn = undefined
|
|
snapshots.lastTakePosition = undefined
|
|
addToSnapshots = false
|
|
}
|
|
|
|
let closeHeaders = ['Trade Number', 'Close Datetime', 'Strategy', 'Trigger On Situation', 'Take Position Situation', 'Result', 'ROI', 'Exit Type']
|
|
if (snapshots.headers === undefined) {
|
|
snapshots.headers = closeHeaders.concat(JSON.parse(JSON.stringify(snapshotLoopHeaders)))
|
|
}
|
|
}
|
|
|
|
function addSimulationRecord() {
|
|
/* Simulation Record */
|
|
let simulationRecord
|
|
|
|
if (variable.current.balance.baseAsset === Infinity) {
|
|
variable.current.balance.baseAsset = Number.MAX_SAFE_INTEGER
|
|
}
|
|
|
|
if (variable.current.balance.quotedAsset === Infinity) {
|
|
variable.current.balance.quotedAsset = Number.MAX_SAFE_INTEGER
|
|
}
|
|
|
|
simulationRecord = [
|
|
candle.begin,
|
|
candle.end,
|
|
variable.current.balance.baseAsset,
|
|
variable.current.balance.quotedAsset,
|
|
variable.episode.stat.profitLoss,
|
|
variable.last.position.profitLoss,
|
|
variable.current.position.stopLoss.value,
|
|
variable.episode.count.positions,
|
|
variable.episode.count.hits,
|
|
variable.episode.count.fails,
|
|
variable.episode.stat.hitRatio,
|
|
variable.episode.stat.ROI,
|
|
variable.episode.count.periods,
|
|
variable.episode.stat.days,
|
|
variable.episode.stat.anualizedRateOfReturn,
|
|
variable.current.position.rate,
|
|
variable.last.position.ROI,
|
|
variable.current.position.takeProfit.value,
|
|
variable.current.position.stopLoss.phase,
|
|
variable.current.position.takeProfit.phase,
|
|
variable.current.position.size,
|
|
variable.episode.parameters.initial.balance.baseAsset,
|
|
variable.episode.parameters.minimum.balance.baseAsset,
|
|
variable.episode.parameters.maximum.balance.baseAsset,
|
|
variable.episode.parameters.initial.balance.quotedAsset,
|
|
variable.episode.parameters.minimum.balance.quotedAsset,
|
|
variable.episode.parameters.maximum.balance.quotedAsset,
|
|
'"' + variable.episode.parameters.baseAsset + '"',
|
|
'"' + variable.episode.parameters.quotedAsset + '"',
|
|
'"' + variable.episode.parameters.marketBaseAsset + '"',
|
|
'"' + variable.episode.parameters.marketQuotedAsset + '"',
|
|
variable.current.position.count.periods,
|
|
variable.current.position.stat.days
|
|
]
|
|
|
|
recordsArray.push(simulationRecord)
|
|
}
|
|
|
|
function addConditionsRecord() {
|
|
/* Prepare the information for the Conditions File */
|
|
let conditionsRecord = [
|
|
candle.begin,
|
|
candle.end,
|
|
variable.current.strategy.index,
|
|
variable.current.position.stopLoss.phase,
|
|
variable.current.position.takeProfit.phase,
|
|
conditionsValues,
|
|
formulasErrors,
|
|
formulasValues
|
|
]
|
|
|
|
conditionsArray.push(conditionsRecord)
|
|
}
|
|
|
|
function addStrategyRecord() {
|
|
/*
|
|
Lets see if there will be an open strategy ...
|
|
Except if we are at the head of the market (remember we skipped the last candle for not being closed.)
|
|
*/
|
|
if (variable.current.strategy.begin !== 0 && variable.current.strategy.end === 0 && currentCandleIndex === candles.length - 2 && lastCandle.end !== lastInstantOfTheDay) {
|
|
variable.current.strategy.status = 2 // This means the strategy is open, i.e. that has a begin but no end.
|
|
variable.current.strategy.end = candle.end
|
|
}
|
|
|
|
/* Prepare the information for the Strategies File */
|
|
if (variable.current.strategy.begin !== 0 && variable.current.strategy.end !== 0) {
|
|
let strategyRecord = [
|
|
variable.current.strategy.begin,
|
|
variable.current.strategy.end,
|
|
variable.current.strategy.status,
|
|
variable.current.strategy.number,
|
|
variable.current.strategy.beginRate,
|
|
variable.current.strategy.endRate,
|
|
'"' + variable.current.strategy.situationName + '"',
|
|
'"' + variable.current.strategy.name + '"'
|
|
]
|
|
|
|
strategiesArray.push(strategyRecord)
|
|
|
|
inializeCurrentStrategy()
|
|
}
|
|
}
|
|
|
|
function addPositionRecord() {
|
|
/*
|
|
Lets see if there will be an open trade ...
|
|
Except if we are at the head of the market (remember we skipped the last candle for not being closed.)
|
|
*/
|
|
if (variable.current.position.begin !== 0 &&
|
|
variable.current.position.end === 0 &&
|
|
currentCandleIndex === candles.length - 2 &&
|
|
lastCandle.end !== lastInstantOfTheDay) {
|
|
|
|
/* This means the trade is open */
|
|
variable.current.position.status = 2
|
|
variable.current.position.end = candle.end
|
|
variable.current.position.endRate = candle.close
|
|
|
|
/* Here we will calculate the ongoing ROI */
|
|
if (variable.episode.parameters.baseAsset === variable.episode.parameters.marketBaseAsset) {
|
|
variable.current.position.stat.ROI = (variable.current.position.rate - candle.close) / variable.current.position.rate * 100
|
|
} else {
|
|
variable.current.position.stat.ROI = (candle.close - variable.current.position.rate) / variable.current.position.rate * 100
|
|
}
|
|
}
|
|
|
|
/* Prepare the information for the Positions File */
|
|
if (variable.current.position.begin !== 0 && variable.current.position.end !== 0) {
|
|
let positionRecord = [
|
|
variable.current.position.begin,
|
|
variable.current.position.end,
|
|
variable.current.position.status,
|
|
variable.current.position.stat.ROI,
|
|
variable.current.position.beginRate,
|
|
variable.current.position.endRate,
|
|
variable.current.position.exitType,
|
|
'"' + variable.current.position.situationName + '"'
|
|
]
|
|
|
|
positionsArray.push(positionRecord)
|
|
|
|
initializeCurrentPosition()
|
|
}
|
|
}
|
|
}
|
|
|
|
function checkAnnouncements(node, value) {
|
|
/*
|
|
Value is an optional parameter that represents the value that the announcement is monitoring for change (for numeric values only).
|
|
If we do receive this value, we will only make the annoucement if the variance is grater than the user pre-defined value
|
|
for this variance.
|
|
*/
|
|
|
|
if (node.announcements !== undefined) {
|
|
for (let i = 0; i < node.announcements.length; i++) {
|
|
let announcement = node.announcements[i]
|
|
let key = node.type + '-' + announcement.name + '-' + announcement.id
|
|
|
|
let lastPeriodAnnounced = -1
|
|
let newAnnouncementRecord = {}
|
|
|
|
for (let j = 0; j < variable.announcements.length; j++) {
|
|
let announcementRecord = variable.announcements[j]
|
|
if (announcementRecord.key === key) {
|
|
lastPeriodAnnounced = announcementRecord.periods
|
|
newAnnouncementRecord = announcementRecord
|
|
break
|
|
}
|
|
}
|
|
|
|
if (variable.episode.count.periods > lastPeriodAnnounced) {
|
|
if (isNaN(value) === false) {
|
|
/* The Value Variation is what tells us how much the value already announced must change in order to annouce it again. */
|
|
let valueVariation
|
|
|
|
let code = announcement.code
|
|
valueVariation = code.valueVariation
|
|
|
|
if (newAnnouncementRecord.value !== undefined && valueVariation !== undefined) {
|
|
let upperLimit = newAnnouncementRecord.value + newAnnouncementRecord.value * valueVariation / 100
|
|
let lowerLimit = newAnnouncementRecord.value - newAnnouncementRecord.value * valueVariation / 100
|
|
if (value > lowerLimit && value < upperLimit) {
|
|
/* There is not enough variation to announce this again. */
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
We store the announcement temporarily at an Array to differ its execution, becasue we need to evaulate its formula
|
|
and at this point in time the potential variables used at the formula are still not set.
|
|
*/
|
|
announcement.value = value
|
|
announcementsToBeMade.push(announcement)
|
|
|
|
/* Next, we will remmeber this announcement was already done, so that it is not announced again in further processing of the same day. */
|
|
if (newAnnouncementRecord.periods !== undefined) {
|
|
newAnnouncementRecord.periods = variable.episode.count.periods
|
|
newAnnouncementRecord.value = value
|
|
} else {
|
|
newAnnouncementRecord = {
|
|
key: key,
|
|
periods: variable.episode.count.periods,
|
|
value: value
|
|
}
|
|
variable.announcements.push(newAnnouncementRecord)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function makeAnnoucements() {
|
|
/* Here we go through all the annoucements that need to be done during this loop, and we just do them. */
|
|
for (let i = 0; i < announcementsToBeMade.length; i++) {
|
|
announcement = announcementsToBeMade[i]
|
|
/* Here we check if there is a formula attached to the annoucement, we evaluate it to get the annoucement text. */
|
|
let formulaValue
|
|
if (announcement.formula !== undefined) {
|
|
try {
|
|
let value = announcement.value
|
|
formulaValue = eval(announcement.formula.code)
|
|
} catch (err) {
|
|
announcement.formula.error = err.message
|
|
}
|
|
}
|
|
announcement.formulaValue = formulaValue
|
|
|
|
if (bot.SESSION.socialBots !== undefined) {
|
|
bot.SESSION.socialBots.announce(announcement)
|
|
}
|
|
}
|
|
}
|
|
|
|
function addCodeToSnapshot(code) {
|
|
if (code === undefined) { return }
|
|
|
|
try {
|
|
let instructionsArray = 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 !== 'atAnyvariable.timeFrame') {
|
|
timeFrame = timeFrame.substring(2, 4) + '-' + timeFrame.substring(4, 7)
|
|
}
|
|
let key = timeFrame + '-' + product + '-' + property
|
|
let existingKey = snapshotKeys.get(key)
|
|
|
|
if (existingKey === undefined) { // means that at the current loop this property of this product was not used before.
|
|
snapshotKeys.set(key, key)
|
|
snapshotLoopHeaders.push(key)
|
|
|
|
let value = eval(instruction)
|
|
snapshotDataRecord.push(value)
|
|
}
|
|
}
|
|
}
|
|
} catch (err) {
|
|
logger.write(MODULE_NAME, '[ERROR] runSimulation -> addCodeToSnapshot -> code = ' + code)
|
|
logger.write(MODULE_NAME, '[ERROR] runSimulation -> addCodeToSnapshot -> err = ' + err.stack)
|
|
callBackFunction(global.DEFAULT_FAIL_RESPONSE)
|
|
}
|
|
}
|
|
}
|
|
|
|
function controlLoop() {
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> controlLoop -> Entering function.') }
|
|
|
|
/* 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
|
|
}
|
|
|
|
currentCandleIndex++
|
|
if (currentCandleIndex < 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() {
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> afterLoop -> Entering function.') }
|
|
|
|
/*
|
|
Before returning we need to see if we have to record some of our counters at the variable.
|
|
To do that, the condition to be met is that this execution must include all candles of the current day.
|
|
*/
|
|
|
|
if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> callback -> recordsArray.length = ' + recordsArray.length) }
|
|
|
|
callback(tradingSystem, snapshots.headers, snapshots.triggerOn, snapshots.takePosition)
|
|
}
|
|
|
|
function getElement(pArray, currentCandle, datasetName) {
|
|
if (pArray === undefined) { return }
|
|
try {
|
|
let element
|
|
for (let i = 0; i < pArray.length; i++) {
|
|
element = pArray[i]
|
|
|
|
if (currentCandle.end === element.end) { // when there is an exact match at the end we take that element
|
|
return element
|
|
} else {
|
|
if (
|
|
i > 0 &&
|
|
element.end > currentCandle.end
|
|
) {
|
|
let previousElement = pArray[i - 1]
|
|
if (previousElement.end < currentCandle.end) {
|
|
return previousElement // If one elements goes into the future of currentCandle, then we stop and take the previous element.
|
|
} else {
|
|
return
|
|
}
|
|
}
|
|
if (
|
|
i === pArray.length - 1 // If we reach the end of the array, then we return the last element.
|
|
&&
|
|
element.end < currentCandle.end
|
|
) {
|
|
return element
|
|
}
|
|
}
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
} catch (err) {
|
|
logger.write(MODULE_NAME, '[ERROR] runSimulation -> err = ' + err.stack)
|
|
callBackFunction(global.DEFAULT_FAIL_RESPONSE)
|
|
}
|
|
}
|
|
}
|
|
|