diff --git a/LowFrequencyTrading/OrdersCalculations.js b/LowFrequencyTrading/OrdersCalculations.js index 103ea1b86..4c985113c 100644 --- a/LowFrequencyTrading/OrdersCalculations.js +++ b/LowFrequencyTrading/OrdersCalculations.js @@ -33,48 +33,54 @@ exports.newOrdersCalculations = function newOrdersCalculations(bot, logger) { tradingEngine = undefined tradingSystem = undefined } - + async function actualSizeCalculation(tradingEngineStage, tradingSystemOrder, tradingEngineOrder, order, applyFeePercentage) { /* When we submit the order to the exchange, we do it specifying the order size - in Base Asset. It is mandatory to be in Base Asset as per CCXT Library API. + in Base Asset. It is mandatory to be in Base Asset as per CCXT Library API and + probably as per the Exchange API too. The exchange might take that size or might change it a little bit. For example, if it does not accept as many decimals as we are sending. - If we identify the situation in which the size accepted is different than the - size we have accounted for, we need to make several adjustments so that the + + We have also seen that for Market Orders, the exchange usually does not take + the size requested exactly. Sometimes is a little bit less or even a little bit + more. For Limit orders it is usually the same. + + If it happens that the size accepted (Actual Size) is different than the + size requested (Size), we need to make several adjustments so that the accounting syncronizes with reality. */ - /* This calculation only happens once, the first time the order is checked. */ + /* This calculation needs to happen only once, the first time the order is checked. */ if (tradingEngineOrder.orderBaseAsset.actualSize.value !== tradingEngineOrder.orderBaseAsset.actualSize.config.initialValue) { return } /* We receive the actual size from the exchange at the order.amount field. */ tradingEngineOrder.orderBaseAsset.actualSize.value = order.amount - /* - If not, we will calculate the Quoted Asset Actual Size too. Note that the amount received - is in Base Asset only. In the next formula, we are unsing the rate of the order. - This rate might be replaced afteerwards for an actual rate. When that happens we will - need to adjust these results. - */ - tradingEngineOrder.orderQuotedAsset.actualSize.value = tradingEngineOrder.orderBaseAsset.actualSize.value * tradingEngineOrder.rate.value - - tradingEngineOrder.orderBaseAsset.actualSize.value = global.PRECISE(tradingEngineOrder.orderBaseAsset.actualSize.value, 10) - tradingEngineOrder.orderQuotedAsset.actualSize.value = global.PRECISE(tradingEngineOrder.orderQuotedAsset.actualSize.value, 10) - - /* - If the exchange accepted the size sent on the request to create the order, - we don't need to do anything else here. - */ - if (tradingEngineOrder.orderBaseAsset.size.value === tradingEngineOrder.orderBaseAsset.actualSize.value) { return } - + recalculateActualSize() recalculateSizePlaced() - function recalculateSizePlaced() { + function recalculateActualSize() { /* - Wee also need to unaccount the previous size placed and correctly account - for the the new actual sizes we now have. + We will recalculate the Quoted Asset Actual Size. This will also give to it an initial value the first time we do this. + In the next formula, we are unsing the rate of the order because we dont know yet the Actual Rate. + This rate might be replaced afteerwards for the Actual Rate when this is calculated again once + the Actual Rate is known. + */ + tradingEngineOrder.orderQuotedAsset.actualSize.value = tradingEngineOrder.orderBaseAsset.actualSize.value * tradingEngineOrder.rate.value + + tradingEngineOrder.orderBaseAsset.actualSize.value = global.PRECISE(tradingEngineOrder.orderBaseAsset.actualSize.value, 10) + tradingEngineOrder.orderQuotedAsset.actualSize.value = global.PRECISE(tradingEngineOrder.orderQuotedAsset.actualSize.value, 10) + } + + function recalculateSizePlaced() { + /* + We will also recalculate the Size Placed that was previously calculated with the order Size, + and now we will do it with the order Actual Size. + + We need to unaccount the previous size placed and correctly account + for the the new actual sizes we now know. */ tradingEngineStage.stageBaseAsset.sizePlaced.value = tradingEngineStage.stageBaseAsset.sizePlaced.value - @@ -104,7 +110,7 @@ exports.newOrdersCalculations = function newOrdersCalculations(bot, logger) { For example we might have sent a rate with too many decimals and the exchange truncate them. Another thing that might happen is that we send a rate that is matching orders already at the order book. In this case we would get an actual rate not only different than the one - we sent, but also, better since the trading engine inside the exchange would match out orders + we sent, but also, better since the trading engine inside the exchange would match orders with the first orders best by price. In the case of Market Orders we don't even send a rate. For all those reason @@ -112,12 +118,13 @@ exports.newOrdersCalculations = function newOrdersCalculations(bot, logger) { */ if (order.average !== undefined) { /* - We will use the average whenever is available. + We will use the average whenever is available. As of today it is not 100% clear when this is + available, but it seems sometimes it is not. In those cases we use the price. */ tradingEngineOrder.orderStatistics.actualRate.value = order.average } else { /* - Otherwise we use the order.price. + We use the order.price when the average is not available. */ tradingEngineOrder.orderStatistics.actualRate.value = order.price } @@ -131,6 +138,7 @@ exports.newOrdersCalculations = function newOrdersCalculations(bot, logger) { if (tradingEngineOrder.orderStatistics.actualRate.value === tradingEngineOrder.rate.value) { return } let previousQuotedAssetActualSize = tradingEngineOrder.orderQuotedAsset.actualSize.value + recalculateActualSize() recalculateSizePlaced() @@ -145,7 +153,10 @@ exports.newOrdersCalculations = function newOrdersCalculations(bot, logger) { got the Order Size in Base Asset, either from direct imput from the user or calculated from the Order Size in Quoted Asset input it by the user. So here we go. */ - tradingEngineOrder.orderQuotedAsset.actualSize.value = tradingEngineOrder.orderBaseAsset.actualSize.value * tradingEngineOrder.orderStatistics.actualRate.value + tradingEngineOrder.orderQuotedAsset.actualSize.value = + tradingEngineOrder.orderBaseAsset.actualSize.value * + tradingEngineOrder.orderStatistics.actualRate.value + tradingEngineOrder.orderQuotedAsset.actualSize.value = global.PRECISE(tradingEngineOrder.orderQuotedAsset.actualSize.value, 10) } @@ -173,7 +184,7 @@ exports.newOrdersCalculations = function newOrdersCalculations(bot, logger) { switch (tradingEngineOrder.type) { case 'Market Order': { await applyFeePercentage( - tradingEngineOrder.orderBaseAsset.feesToBePaid, + 'feesToBePaid', tradingEngineOrder, tradingSystemOrder, sessionParameters.feeStructure.config.taker, @@ -183,7 +194,7 @@ exports.newOrdersCalculations = function newOrdersCalculations(bot, logger) { } case 'Limit Order': { await applyFeePercentage( - tradingEngineOrder.orderBaseAsset.feesToBePaid, + 'feesToBePaid', tradingEngineOrder, tradingSystemOrder, sessionParameters.feeStructure.config.maker, @@ -209,24 +220,27 @@ exports.newOrdersCalculations = function newOrdersCalculations(bot, logger) { async function percentageFilledCalculation(tradingEngineStage, tradingSystemOrder, tradingEngineOrder, order, applyFeePercentage) { /* - Percentage Filled Calculation: The only information we get from the exchange is the order.filled. - We know this field will go up until reaching the Size To Be Filled. Once it reaches that number + Percentage Filled Calculation: The only relevant information we get from the exchange is the order.filled. + We know this field will go up until reaching the Actual Size. Once it reaches that number the order will be 100% filled. */ - tradingEngineOrder.orderStatistics.percentageFilled.value = order.filled * 100 / (tradingEngineOrder.orderBaseAsset.sizeToBeFilled.value) + tradingEngineOrder.orderStatistics.percentageFilled.value = order.filled * 100 / tradingEngineOrder.orderBaseAsset.actualSize.value tradingEngineOrder.orderStatistics.percentageFilled.value = global.PRECISE(tradingEngineOrder.orderStatistics.percentageFilled.value, 10) } async function feesPaidCalculation(tradingEngineStage, tradingSystemOrder, tradingEngineOrder, order, applyFeePercentage) { /* - The exchange fees are taken from the Base Asset or the Quoted Asset depending if we - are buying or selling. Within the order information received from the exchange we can not see the fees paid so we need to - calculate them ourselves. To do this it is needed to be used the Session Fees Parameters. + Within the order information received from the exchange we can not see the fees paid so we need to + calculate them ourselves. Knwowing the Fees Paid is critical to later update the balances correctly, + since at every trade we will receive less than just the Actual Size. In fact we will receive the Actual + Size minus the Fees Paid. + + The percentage at which the exchange will charge fees needs to be configured at the the Session Fees Parameters. */ switch (tradingEngineOrder.type) { case 'Market Order': { await applyFeePercentage( - tradingEngineOrder.orderBaseAsset.feesPaid, + 'feesPaid', tradingEngineOrder, tradingSystemOrder, sessionParameters.feeStructure.config.taker, @@ -236,7 +250,7 @@ exports.newOrdersCalculations = function newOrdersCalculations(bot, logger) { } case 'Limit Order': { await applyFeePercentage( - tradingEngineOrder.orderBaseAsset.feesPaid, + 'feesPaid', tradingEngineOrder, tradingSystemOrder, sessionParameters.feeStructure.config.maker, @@ -251,18 +265,16 @@ exports.newOrdersCalculations = function newOrdersCalculations(bot, logger) { /* CCXT returns order.filled with an amount denominated in Base Asset. We will take it from there for our Order Base Asset. The amount in order.filled does - not account for the fees, which is exactly what we need in this case. + not reflect the Fees Paid, even if we are paying the fees in Base Asset. */ tradingEngineOrder.orderBaseAsset.sizeFilled.value = order.filled tradingEngineOrder.orderBaseAsset.sizeFilled.value = global.PRECISE(tradingEngineOrder.orderBaseAsset.sizeFilled.value, 10) /* For Quoted Asset, CCXT does not return any field with the size filled, so we - need to calculate that by ourselves. Since we know the Size in Quoted Asset thet - is already adjusted with the Actual Rate and the Percentage Filled, we will use all - this in this formula to get the Size Filled in Quoted Asset. + need to calculate that by ourselves. */ tradingEngineOrder.orderQuotedAsset.sizeFilled.value = - tradingEngineOrder.orderQuotedAsset.sizeToBeFilled.value * tradingEngineOrder.orderStatistics.percentageFilled.value / 100 + tradingEngineOrder.orderQuotedAsset.sizeFilled.value * tradingEngineOrder.orderStatistics.percentageFilled.value / 100 tradingEngineOrder.orderQuotedAsset.sizeFilled.value = global.PRECISE(tradingEngineOrder.orderQuotedAsset.sizeFilled.value, 10) } diff --git a/LowFrequencyTrading/TradingOrders.js b/LowFrequencyTrading/TradingOrders.js index 51969100b..ec3eb78a2 100644 --- a/LowFrequencyTrading/TradingOrders.js +++ b/LowFrequencyTrading/TradingOrders.js @@ -665,64 +665,58 @@ exports.newTradingOrders = function newTradingOrders(bot, logger, tradingEngineM /* Balances Update */ switch (true) { /* - Let's remember that for Buy orders, the fees are paid in Base Asset.So we - need to substract from the balance of Quoted Asset both the size filled and - the fees, and we will receive at the balance of Base Asset just the size filled, - bacuase the exchange gets the fees in Base Asset for them. - */ - case tradingSystemOrder.type === 'Market Buy Order' || tradingSystemOrder.type === 'Limit Buy Order': { - - /* Balance Base Asset: Undo the previous accounting */ - tradingEngine.current.episode.episodeBaseAsset.balance.value = - tradingEngine.current.episode.episodeBaseAsset.balance.value - - previousBaseAssetSizeFilled - - /* Balance Base Asset: Account the current filling and fees */ - tradingEngine.current.episode.episodeBaseAsset.balance.value = - tradingEngine.current.episode.episodeBaseAsset.balance.value + - tradingEngineOrder.orderBaseAsset.sizeFilled.value - - /* Balance Quoted Asset: Undo the previous accounting */ - tradingEngine.current.episode.episodeQuotedAsset.balance.value = - tradingEngine.current.episode.episodeQuotedAsset.balance.value + - previousQuotedAssetSizeFilled + - previousQuotedAssetFeesPaid - - /* Balance Quoted Asset: Account the current filling and fees */ - tradingEngine.current.episode.episodeQuotedAsset.balance.value = - tradingEngine.current.episode.episodeQuotedAsset.balance.value - - tradingEngineOrder.orderQuotedAsset.sizeFilled.value - - tradingEngineOrder.orderQuotedAsset.feesPaid.value - break - } - /* - For Sell orders, the fees are being paid in Quote Asset, so we will substract - from the Base Asset balance the size filled and the fees, and we will add - to the Quoted Asset balance only the size filled, because the exchange - get those fees. + For Sell orders, the fees are being paid in Quote Asset. */ case tradingSystemOrder.type === 'Market Sell Order' || tradingSystemOrder.type === 'Limit Sell Order': { /* Balance Base Asset: Undo the previous accounting */ tradingEngine.current.episode.episodeBaseAsset.balance.value = tradingEngine.current.episode.episodeBaseAsset.balance.value + + previousBaseAssetSizeFilled + + /* Balance Base Asset: Account the current filling and fees */ + tradingEngine.current.episode.episodeBaseAsset.balance.value = + tradingEngine.current.episode.episodeBaseAsset.balance.value - + tradingEngineOrder.orderBaseAsset.sizeFilled.value + + /* Balance Quoted Asset: Undo the previous accounting */ + tradingEngine.current.episode.episodeQuotedAsset.balance.value = + tradingEngine.current.episode.episodeQuotedAsset.balance.value - + previousQuotedAssetSizeFilled + + previousQuotedAssetFeesPaid + + /* Balance Quoted Asset: Account the current filling and fees */ + tradingEngine.current.episode.episodeQuotedAsset.balance.value = + tradingEngine.current.episode.episodeQuotedAsset.balance.value + + tradingEngineOrder.orderQuotedAsset.sizeFilled.value - + tradingEngineOrder.orderQuotedAsset.feesPaid.value + break + } + /* + Let's remember that for Buy orders, the fees are paid in Base Asset. + */ + case tradingSystemOrder.type === 'Market Buy Order' || tradingSystemOrder.type === 'Limit Buy Order': { + + /* Balance Base Asset: Undo the previous accounting */ + tradingEngine.current.episode.episodeBaseAsset.balance.value = + tradingEngine.current.episode.episodeBaseAsset.balance.value - previousBaseAssetSizeFilled + previousBaseAssetFeesPaid /* Balance Base Asset: Account the current filling and fees */ tradingEngine.current.episode.episodeBaseAsset.balance.value = - tradingEngine.current.episode.episodeBaseAsset.balance.value - + tradingEngine.current.episode.episodeBaseAsset.balance.value + tradingEngineOrder.orderBaseAsset.sizeFilled.value - tradingEngineOrder.orderBaseAsset.feesPaid.value /* Balance Quoted Asset: Undo the previous accounting */ tradingEngine.current.episode.episodeQuotedAsset.balance.value = - tradingEngine.current.episode.episodeQuotedAsset.balance.value - + tradingEngine.current.episode.episodeQuotedAsset.balance.value + previousQuotedAssetSizeFilled /* Balance Quoted Asset: Account the current filling and fees */ tradingEngine.current.episode.episodeQuotedAsset.balance.value = - tradingEngine.current.episode.episodeQuotedAsset.balance.value + + tradingEngine.current.episode.episodeQuotedAsset.balance.value - tradingEngineOrder.orderQuotedAsset.sizeFilled.value break } @@ -848,12 +842,19 @@ exports.newTradingOrders = function newTradingOrders(bot, logger, tradingEngineM tradingEngineStage.stageQuotedAsset.sizePlaced.value = global.PRECISE(tradingEngineStage.stageQuotedAsset.sizePlaced.value, 10) } - async function applyFeePercentage(feesNode, tradingEngineOrder, tradingSystemOrder, feePercentage, percentageFilled) { + async function applyFeePercentage(feesNodePropertyName, tradingEngineOrder, tradingSystemOrder, feePercentage, percentageFilled) { + /* + The exchange fees are taken from the Base Asset or the Quoted Asset depending if we + are buying or selling. + */ + let feesNode + switch (true) { case tradingSystemOrder.type === 'Market Buy Order' || tradingSystemOrder.type === 'Limit Buy Order': { /* In this case the fees are taken from the Base Asset you receive as a result of the trade at the exchange. */ + feesNode = tradingEngineOrder.orderBaseAsset[feesNodePropertyName] feesNode.value = tradingEngineOrder.orderBaseAsset.actualSize.value * feePercentage / 100 * @@ -864,6 +865,7 @@ exports.newTradingOrders = function newTradingOrders(bot, logger, tradingEngineM /* In this case the fees are taken from the Quoted Asset you receive as a result of the trade at the exchange. */ + feesNode = tradingEngineOrder.orderQuotedAsset[feesNodePropertyName] feesNode.value = tradingEngineOrder.orderQuotedAsset.actualSize.value * feePercentage / 100 * @@ -872,8 +874,8 @@ exports.newTradingOrders = function newTradingOrders(bot, logger, tradingEngineM } } - tradingEngineOrder.orderBaseAsset.feesPaid.value = global.PRECISE(tradingEngineOrder.orderBaseAsset.feesPaid.value, 10) - tradingEngineOrder.orderQuotedAsset.feesPaid.value = global.PRECISE(tradingEngineOrder.orderQuotedAsset.feesPaid.value, 10) + feesNode.value = global.PRECISE(feesNode.value, 10) + feesNode.value = global.PRECISE(feesNode.value, 10) } async function checkOrderEvent(event, order, executionAlgorithm, executionNode) { diff --git a/LowFrequencyTrading/TradingStages.js b/LowFrequencyTrading/TradingStages.js index 774e1c1d2..7c95d8ac7 100644 --- a/LowFrequencyTrading/TradingStages.js +++ b/LowFrequencyTrading/TradingStages.js @@ -888,14 +888,18 @@ exports.newTradingStages = function newTradingStages(bot, logger, tradingEngineM function checkIfStageNeedsToBeClosed(tradingEngineStage, tradingSystemStage, stageName) { /* - The Stage is closed when the placedSize reaches the targetSize, and the fillSize - + feesPaid reaches the placedSize. This depends on how the user defined the stage: - either on Base Asset or Quoted Asset. - - This can happens at any time when we update the sizeFilled and the feesPaid values - when we see at the exchange that orders were filled. - - Note that the comparison is made with a Rounding Factor in order to avoid rounding problems. + The Stage is closed when the fillSize + feesPaid reaches the targetSize. + + The Target Size is defined by the end user either in Base Asset or Quoted Asset. + Whatever the user chooses defines also the way we check if the Target was met. + Why? Because the counterparty target is estimated with the Target Rate and it + is not syncronized with the reality we later learn when the exchange tell us the + actual rate each of the orders where executed at. + + For the same reason that the user does not know exactly which is the rate the orders + will actually be filled, then we can not expect that the total filled will really + reach the target size, so we introduce a rounding factor so that when it is close + enough we will consider the target to have been reached. */ const ROUNDING_ERROR_CORRECTION_FACTOR = 1.001 @@ -903,8 +907,7 @@ exports.newTradingStages = function newTradingStages(bot, logger, tradingEngineM case 'Base Asset': { if ( ( - tradingEngineStage.stageBaseAsset.sizeFilled.value + - tradingEngineStage.stageBaseAsset.feesPaid.value + tradingEngineStage.stageBaseAsset.sizeFilled.value ) * ROUNDING_ERROR_CORRECTION_FACTOR >= @@ -919,8 +922,7 @@ exports.newTradingStages = function newTradingStages(bot, logger, tradingEngineM case 'Quoted Asset': { if ( ( - tradingEngineStage.stageQuotedAsset.sizeFilled.value + - tradingEngineStage.stageQuotedAsset.feesPaid.value + tradingEngineStage.stageQuotedAsset.sizeFilled.value ) * ROUNDING_ERROR_CORRECTION_FACTOR >=