432 lines
22 KiB
JavaScript
432 lines
22 KiB
JavaScript
exports.newTradingPosition = function newTradingPosition(bot, logger, tradingEngineModule) {
|
|
/*
|
|
This module packages all functions related to Positions.
|
|
*/
|
|
const MODULE_NAME = 'Trading Position'
|
|
let thisObject = {
|
|
mantain: mantain,
|
|
reset: reset,
|
|
cycleBasedStatistics: cycleBasedStatistics,
|
|
openPosition: openPosition,
|
|
closingPosition: closingPosition,
|
|
closePosition: closePosition,
|
|
applyStopLossFormula: applyStopLossFormula,
|
|
applyTakeProfitFormula: applyTakeProfitFormula,
|
|
updateStopLoss: updateStopLoss,
|
|
updateTakeProfit: updateTakeProfit,
|
|
initialTargets: initialTargets,
|
|
updateEnds: updateEnds,
|
|
resetTradingEngineDataStructure: resetTradingEngineDataStructure,
|
|
updateCounters: updateCounters,
|
|
initialize: initialize,
|
|
finalize: finalize
|
|
}
|
|
|
|
let tradingEngine
|
|
let tradingSystem
|
|
let sessionParameters
|
|
|
|
return thisObject
|
|
|
|
function initialize() {
|
|
tradingSystem = bot.simulationState.tradingSystem
|
|
tradingEngine = bot.simulationState.tradingEngine
|
|
sessionParameters = bot.SESSION.parameters
|
|
}
|
|
|
|
function finalize() {
|
|
tradingEngine = undefined
|
|
tradingSystem = undefined
|
|
sessionParameters = undefined
|
|
}
|
|
|
|
function mantain() {
|
|
updateCounters()
|
|
updateEnds()
|
|
}
|
|
|
|
function reset() {
|
|
resetTradingEngineDataStructure()
|
|
}
|
|
|
|
function openPosition(situationName) {
|
|
|
|
/* Starting begin and end */
|
|
tradingEngine.current.position.begin.value = tradingEngine.current.episode.cycle.lastBegin.value
|
|
tradingEngine.current.position.end.value = tradingEngine.current.episode.cycle.lastEnd.value
|
|
|
|
/* Recording the opening at the Trading Engine Data Structure */
|
|
tradingEngine.current.position.status.value = 'Open'
|
|
tradingEngine.current.position.serialNumber.value = tradingEngine.current.episode.episodeCounters.positions.value + 1
|
|
tradingEngine.current.position.identifier.value = global.UNIQUE_ID()
|
|
tradingEngine.current.position.beginRate.value = tradingEngine.current.episode.candle.close.value
|
|
tradingEngine.current.position.positionBaseAsset.beginBalance.value = tradingEngine.current.episode.episodeBaseAsset.balance.value
|
|
tradingEngine.current.position.positionQuotedAsset.beginBalance.value = tradingEngine.current.episode.episodeQuotedAsset.balance.value
|
|
tradingEngine.current.position.situationName.value = situationName
|
|
|
|
/* Initializing Stop and Take Phase */
|
|
tradingEngine.current.position.stopLoss.stopLossPhase.value = 1
|
|
tradingEngine.current.position.takeProfit.takeProfitPhase.value = 1
|
|
|
|
/* Updating Episode Counters */
|
|
tradingEngine.current.episode.episodeCounters.positions.value++
|
|
|
|
/* Inicializing this counter */
|
|
tradingEngine.current.episode.distanceToEvent.takePosition.value = 1
|
|
|
|
/* Remember the balance we had before taking the position to later calculate profit or loss */
|
|
tradingEngine.current.position.positionBaseAsset.beginBalance = tradingEngine.current.episode.episodeBaseAsset.balance.value
|
|
tradingEngine.current.position.positionQuotedAsset.beginBalance = tradingEngine.current.episode.episodeQuotedAsset.balance.value
|
|
}
|
|
|
|
function closingPosition(exitType) {
|
|
tradingEngine.current.position.status.value = 'Closing'
|
|
tradingEngine.current.position.exitType.value = exitType
|
|
|
|
/*
|
|
By the time we figured out that the stop loss or take profit were hit, or
|
|
for whatever reason the position needs to be closed, the take profit and
|
|
stop loss values for the next candle have already been calculated. In order
|
|
to avoid being plotted, we will put them in zero.
|
|
*/
|
|
tradingEngine.current.position.stopLoss.value = 0
|
|
tradingEngine.current.position.takeProfit.value = 0
|
|
}
|
|
|
|
function closePosition() {
|
|
tradingEngine.current.position.status.value = 'Closed'
|
|
tradingEngine.current.position.end.value = tradingEngine.current.episode.cycle.lastEnd.value
|
|
tradingEngine.current.position.endRate.value = tradingEngine.current.episode.candle.close.value
|
|
tradingEngine.current.position.positionBaseAsset.endBalance.value = tradingEngine.current.episode.episodeBaseAsset.balance.value
|
|
tradingEngine.current.position.positionQuotedAsset.endBalance.value = tradingEngine.current.episode.episodeQuotedAsset.balance.value
|
|
|
|
/*
|
|
Now that the position is closed, it is the right time to move this position from current to last at the Trading Engine data structure.
|
|
*/
|
|
tradingEngineModule.cloneValues(tradingEngine.current.position, tradingEngine.last.position)
|
|
|
|
cycleBasedStatistics()
|
|
|
|
/* Updating Hits & Fails */
|
|
if (tradingEngine.current.position.positionBaseAsset.profitLoss.value > 0) {
|
|
tradingEngine.current.episode.episodeBaseAsset.hits.value++
|
|
}
|
|
if (tradingEngine.current.position.positionBaseAsset.profitLoss.value < 0) {
|
|
tradingEngine.current.episode.episodeBaseAsset.fails.value++
|
|
}
|
|
if (tradingEngine.current.position.positionQuotedAsset.profitLoss.value > 0) {
|
|
tradingEngine.current.episode.episodeQuotedAsset.hits.value++
|
|
}
|
|
if (tradingEngine.current.position.positionQuotedAsset.profitLoss.value < 0) {
|
|
tradingEngine.current.episode.episodeQuotedAsset.fails.value++
|
|
}
|
|
}
|
|
|
|
function applyStopLossFormula(formulas, formulaId) {
|
|
updateStopLossTakeProfitFinalValue(tradingEngine.current.position.stopLoss)
|
|
tradingEngine.current.position.stopLoss.value = formulas.get(formulaId)
|
|
updateStopLossTakeProfitInitialValue(tradingEngine.current.position.stopLoss)
|
|
updateStopLossTakeProfitBeginEnd(tradingEngine.current.position.stopLoss)
|
|
}
|
|
|
|
function applyTakeProfitFormula(formulas, formulaId) {
|
|
updateStopLossTakeProfitFinalValue(tradingEngine.current.position.takeProfit)
|
|
tradingEngine.current.position.takeProfit.value = formulas.get(formulaId)
|
|
updateStopLossTakeProfitInitialValue(tradingEngine.current.position.takeProfit)
|
|
updateStopLossTakeProfitBeginEnd(tradingEngine.current.position.takeProfit)
|
|
}
|
|
|
|
function updateStopLossTakeProfitInitialValue(node) {
|
|
/*
|
|
We store the first Stop Loss or Take Profit value in a separate node.
|
|
To know if it is the first one we compare its current value to the initial value.
|
|
*/
|
|
if (node.initialValue.value === node.initialValue.config.initialValue) {
|
|
node.initialValue.value = node.value
|
|
}
|
|
}
|
|
|
|
function updateStopLossTakeProfitFinalValue(node) {
|
|
/*
|
|
We set the final value just before calcualting the new value, since the new calculated
|
|
value is going to be discarded if the position is closed after it is calculated, but
|
|
during the same candle.
|
|
*/
|
|
node.finalValue.value = node.value
|
|
}
|
|
|
|
function updateStopLossTakeProfitBeginEnd(node) {
|
|
/*
|
|
Both the Stop Loss and the Take Profit have their own Begin and End.
|
|
Ther reason for this is because they represent targets to be checked
|
|
at the next candle, so it does not apply that they share the begin and
|
|
end of the position itself.
|
|
*/
|
|
node.begin.value =
|
|
tradingEngine.current.episode.candle.begin.value +
|
|
sessionParameters.timeFrame.config.value
|
|
node.end.value =
|
|
tradingEngine.current.episode.candle.end.value +
|
|
sessionParameters.timeFrame.config.value
|
|
}
|
|
|
|
function updateStopLoss(phase) {
|
|
tradingEngine.current.position.stopLoss.stopLossPhase.value = phase
|
|
}
|
|
|
|
function updateTakeProfit(phase) {
|
|
tradingEngine.current.position.takeProfit.takeProfitPhase.value = phase
|
|
}
|
|
|
|
function initialTargets(tradingSystemStageNode, tradingEngineStageNode) {
|
|
|
|
if (tradingSystemStageNode.initialTargets === undefined) {
|
|
const message = 'Stage without Initial Targets node. Add one please.'
|
|
badDefinitionUnhandledException(undefined, message, tradingSystemStageNode)
|
|
}
|
|
|
|
setTargetRate()
|
|
setTargetSize()
|
|
|
|
function setTargetRate() {
|
|
if (tradingSystemStageNode.initialTargets.targetRate === undefined) {
|
|
const message = 'Target Rate Node not Found. Fix this please.'
|
|
badDefinitionUnhandledException(undefined, message, tradingSystemStageNode.initialTargets)
|
|
}
|
|
if (tradingSystemStageNode.initialTargets.targetRate.formula === undefined) {
|
|
const message = 'Formula of Target Rate Node not Found. Fix this please.'
|
|
badDefinitionUnhandledException(undefined, message, tradingSystemStageNode.initialTargets.targetRate)
|
|
}
|
|
|
|
let value = tradingSystem.formulas.get(tradingSystemStageNode.initialTargets.targetRate.formula.id)
|
|
if (value === undefined) {
|
|
const message = 'Target Rate can not be undefined. Fix this please.'
|
|
badDefinitionUnhandledException(undefined, message, tradingSystemStageNode.initialTargets.targetRate.formula)
|
|
}
|
|
if (isNaN(value)) {
|
|
const message = 'Target Rate must be a number. Fix this please.'
|
|
badDefinitionUnhandledException(undefined, message, tradingSystemStageNode.initialTargets.targetRate.formula)
|
|
}
|
|
|
|
switch (tradingSystemStageNode.type) {
|
|
case 'Open Stage': {
|
|
tradingEngine.current.position.entryTargetRate.value = global.PRECISE(value, 10)
|
|
break
|
|
}
|
|
case 'Close Stage': {
|
|
tradingEngine.current.position.exitTargetRate.value = global.PRECISE(value, 10)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
function setTargetSize() {
|
|
/* Basic Validation */
|
|
if (
|
|
tradingSystemStageNode.initialTargets.targetSizeInBaseAsset !== undefined &&
|
|
tradingSystemStageNode.initialTargets.targetSizeInQuotedAsset !== undefined
|
|
) {
|
|
const message = 'Only Target Size In Base Asset or Target Size In Quoted Asset is allowed. Remove one of them please.'
|
|
badDefinitionUnhandledException(undefined, message, tradingSystemStageNode)
|
|
}
|
|
|
|
/* Position In Base Asset */
|
|
if (tradingSystemStageNode.initialTargets.targetSizeInBaseAsset !== undefined) {
|
|
if (tradingSystemStageNode.initialTargets.targetSizeInBaseAsset.formula !== undefined) {
|
|
let value = tradingSystem.formulas.get(tradingSystemStageNode.initialTargets.targetSizeInBaseAsset.formula.id)
|
|
if (value === undefined) {
|
|
const message = 'Target Size In Base Asset cannot be undefined. Fix this please.'
|
|
badDefinitionUnhandledException(undefined, message, tradingSystemStageNode.initialTargets.targetSizeInBaseAsset.formula)
|
|
}
|
|
if (value === 0) {
|
|
const message = 'Target Size In Base Asset cannot be zero. Fix this please.'
|
|
badDefinitionUnhandledException(undefined, message, tradingSystemStageNode.initialTargets.targetSizeInBaseAsset.formula)
|
|
}
|
|
|
|
switch (tradingSystemStageNode.type) {
|
|
case 'Open Stage': {
|
|
tradingEngine.current.position.positionBaseAsset.entryTargetSize.value = global.PRECISE(value, 10)
|
|
tradingEngine.current.position.positionQuotedAsset.entryTargetSize.value =
|
|
global.PRECISE(value * tradingEngine.current.position.entryTargetRate.value, 10)
|
|
break
|
|
}
|
|
case 'Close Stage': {
|
|
tradingEngine.current.position.positionBaseAsset.exitTargetSize.value = global.PRECISE(value, 10)
|
|
tradingEngine.current.position.positionQuotedAsset.exitTargetSize.value =
|
|
global.PRECISE(value * tradingEngine.current.position.exitTargetRate.value, 10)
|
|
break
|
|
}
|
|
}
|
|
|
|
/* Remember how the end user defined this stage. */
|
|
tradingEngineStageNode.stageDefinedIn.value = 'Base Asset'
|
|
} else {
|
|
const message = 'You need to specify a Formula for this.'
|
|
badDefinitionUnhandledException(undefined, message, tradingSystemStageNode.initialTargets.targetSizeInBaseAsset)
|
|
}
|
|
}
|
|
|
|
/* Position In Quoted Asset */
|
|
if (tradingSystemStageNode.initialTargets.targetSizeInQuotedAsset !== undefined) {
|
|
if (tradingSystemStageNode.initialTargets.targetSizeInQuotedAsset.formula !== undefined) {
|
|
let value = tradingSystem.formulas.get(tradingSystemStageNode.initialTargets.targetSizeInQuotedAsset.formula.id)
|
|
if (value === undefined) {
|
|
const message = 'Target Size In Quoted Asset cannot be undefined. Fix this please.'
|
|
badDefinitionUnhandledException(undefined, message, tradingSystemStageNode.initialTargets.targetSizeInQuotedAsset.formula)
|
|
}
|
|
if (value === 0) {
|
|
const message = 'Target Size In Quoted Asset cannot be zero. Fix this please.'
|
|
badDefinitionUnhandledException(undefined, message, tradingSystemStageNode.initialTargets.targetSizeInQuotedAsset.formula)
|
|
}
|
|
switch (tradingSystemStageNode.type) {
|
|
case 'Open Stage': {
|
|
tradingEngine.current.position.positionQuotedAsset.entryTargetSize.value = global.PRECISE(value, 10)
|
|
tradingEngine.current.position.positionBaseAsset.entryTargetSize.value =
|
|
global.PRECISE(value / tradingEngine.current.position.entryTargetRate.value, 10)
|
|
break
|
|
}
|
|
case 'Close Stage': {
|
|
tradingEngine.current.position.positionQuotedAsset.exitTargetSize.value = global.PRECISE(value, 10)
|
|
tradingEngine.current.position.positionBaseAsset.exitTargetSize.value =
|
|
global.PRECISE(value / tradingEngine.current.position.exitTargetRate.value, 10)
|
|
break
|
|
}
|
|
}
|
|
|
|
/* Remember how the end user defined this stage. */
|
|
tradingEngineStageNode.stageDefinedIn.value = 'Quoted Asset'
|
|
} else {
|
|
const message = 'You need to specify a Formula for this.'
|
|
badDefinitionUnhandledException(undefined, message, tradingSystemStageNode.initialTargets.targetSizeInQuotedAsset)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function updateEnds() {
|
|
if (tradingEngine.current.position.status.value === 'Open') {
|
|
tradingEngine.current.position.end.value = tradingEngine.current.position.end.value + sessionParameters.timeFrame.config.value
|
|
tradingEngine.current.position.endRate.value = tradingEngine.current.episode.candle.close.value
|
|
tradingEngine.current.position.positionBaseAsset.endBalance.value = tradingEngine.current.episode.episodeBaseAsset.balance.value
|
|
tradingEngine.current.position.positionQuotedAsset.endBalance.value = tradingEngine.current.episode.episodeQuotedAsset.balance.value
|
|
}
|
|
}
|
|
|
|
function resetTradingEngineDataStructure() {
|
|
if (tradingEngine.current.position.status.value === 'Closed') {
|
|
tradingEngineModule.initializeNode(tradingEngine.current.position)
|
|
}
|
|
}
|
|
|
|
function updateCounters() {
|
|
if (tradingEngine.current.position.status.value === 'Open') {
|
|
tradingEngine.current.position.positionCounters.periods.value++
|
|
}
|
|
}
|
|
|
|
function cycleBasedStatistics() {
|
|
|
|
calculateAssetsStatistics()
|
|
calculatePositionStatistics()
|
|
|
|
function calculateAssetsStatistics() {
|
|
/* Profit Loss Calculation */
|
|
tradingEngine.current.position.positionBaseAsset.profitLoss.value =
|
|
tradingEngine.current.episode.episodeBaseAsset.balance.value -
|
|
tradingEngine.current.position.positionBaseAsset.beginBalance
|
|
|
|
tradingEngine.current.position.positionQuotedAsset.profitLoss.value =
|
|
tradingEngine.current.episode.episodeQuotedAsset.balance.value -
|
|
tradingEngine.current.position.positionQuotedAsset.beginBalance
|
|
|
|
tradingEngine.current.position.positionBaseAsset.profitLoss.value = global.PRECISE(tradingEngine.current.position.positionBaseAsset.profitLoss.value, 10)
|
|
tradingEngine.current.position.positionQuotedAsset.profitLoss.value = global.PRECISE(tradingEngine.current.position.positionQuotedAsset.profitLoss.value, 10)
|
|
|
|
/* ROI Calculation */
|
|
tradingEngine.current.position.positionBaseAsset.ROI.value =
|
|
tradingEngine.current.position.positionBaseAsset.profitLoss.value * 100 /
|
|
tradingEngine.current.strategyOpenStage.stageBaseAsset.targetSize.value
|
|
|
|
tradingEngine.current.position.positionQuotedAsset.ROI.value =
|
|
tradingEngine.current.position.positionQuotedAsset.profitLoss.value * 100 /
|
|
tradingEngine.current.strategyOpenStage.stageQuotedAsset.targetSize.value
|
|
|
|
tradingEngine.current.position.positionBaseAsset.ROI.value = global.PRECISE(tradingEngine.current.position.positionBaseAsset.ROI.value, 10)
|
|
tradingEngine.current.position.positionQuotedAsset.ROI.value = global.PRECISE(tradingEngine.current.position.positionQuotedAsset.ROI.value, 10)
|
|
|
|
/* Hit Fail Calculation */
|
|
if (tradingEngine.current.position.positionBaseAsset.ROI.value > 0) {
|
|
tradingEngine.current.position.positionBaseAsset.hitFail.value = 'Hit'
|
|
}
|
|
if (tradingEngine.current.position.positionBaseAsset.ROI.value < 0) {
|
|
tradingEngine.current.position.positionBaseAsset.hitFail.value = 'Fail'
|
|
}
|
|
if (tradingEngine.current.position.positionBaseAsset.ROI.value === 0) {
|
|
tradingEngine.current.position.positionBaseAsset.hitFail.value = 'Even'
|
|
}
|
|
if (tradingEngine.current.position.positionQuotedAsset.ROI.value > 0) {
|
|
tradingEngine.current.position.positionQuotedAsset.hitFail.value = 'Hit'
|
|
}
|
|
if (tradingEngine.current.position.positionQuotedAsset.ROI.value < 0) {
|
|
tradingEngine.current.position.positionQuotedAsset.hitFail.value = 'Fail'
|
|
}
|
|
if (tradingEngine.current.position.positionQuotedAsset.ROI.value === 0) {
|
|
tradingEngine.current.position.positionQuotedAsset.hitFail.value = 'Even'
|
|
}
|
|
}
|
|
|
|
function calculatePositionStatistics() {
|
|
/* Profit Loss Calculation */
|
|
tradingEngine.current.position.positionStatistics.profitLoss.value =
|
|
(
|
|
tradingEngine.current.episode.episodeBaseAsset.profitLoss.value * tradingEngine.current.position.endRate.value +
|
|
tradingEngine.current.episode.episodeQuotedAsset.profitLoss.value
|
|
)
|
|
|
|
tradingEngine.current.position.positionStatistics.profitLoss.value = global.PRECISE(tradingEngine.current.position.positionStatistics.profitLoss.value, 10)
|
|
|
|
/* ROI Calculation */
|
|
tradingEngine.current.position.positionStatistics.ROI.value =
|
|
(
|
|
tradingEngine.current.position.positionStatistics.profitLoss.value
|
|
)
|
|
* 100 /
|
|
(
|
|
tradingEngine.current.position.positionBaseAsset.beginBalance * tradingEngine.current.position.beginRate.value +
|
|
tradingEngine.current.position.positionQuotedAsset.beginBalance
|
|
)
|
|
tradingEngine.current.position.positionStatistics.ROI.value = global.PRECISE(tradingEngine.current.position.positionStatistics.ROI.value, 10)
|
|
|
|
/* Hit Fail Calculation */
|
|
if (tradingEngine.current.position.positionStatistics.ROI.value > 0) {
|
|
tradingEngine.current.position.positionStatistics.hitFail.value = 'Hit'
|
|
}
|
|
if (tradingEngine.current.position.positionStatistics.ROI.value < 0) {
|
|
tradingEngine.current.position.positionStatistics.hitFail.value = 'Fail'
|
|
}
|
|
if (tradingEngine.current.position.positionStatistics.ROI.value === 0) {
|
|
tradingEngine.current.position.positionStatistics.hitFail.value = 'Even'
|
|
}
|
|
|
|
/* Days Calculation */
|
|
tradingEngine.current.position.positionStatistics.days.value =
|
|
tradingEngine.current.position.positionCounters.periods.value *
|
|
sessionParameters.timeFrame.config.value / global.ONE_DAY_IN_MILISECONDS
|
|
|
|
tradingEngine.current.position.positionStatistics.days.value = global.PRECISE(tradingEngine.current.position.positionStatistics.days.value, 10)
|
|
}
|
|
}
|
|
|
|
function badDefinitionUnhandledException(err, message, node) {
|
|
tradingSystem.errors.push([node.id, message])
|
|
|
|
logger.write(MODULE_NAME, "[ERROR] -> " + message);
|
|
logger.write(MODULE_NAME, "[ERROR] -> node.name = " + node.name);
|
|
logger.write(MODULE_NAME, "[ERROR] -> node.type = " + node.type);
|
|
logger.write(MODULE_NAME, "[ERROR] -> node.config = " + JSON.stringify(node.config));
|
|
if (err !== undefined) {
|
|
logger.write(MODULE_NAME, "[ERROR] -> err.stack = " + err.stack);
|
|
}
|
|
throw 'Please fix the problem and try again.'
|
|
}
|
|
} |