433 lines
23 KiB
JavaScript
433 lines
23 KiB
JavaScript
exports.newOrdersSimulations = function (processIndex, bot, logger) {
|
|
/*
|
|
When we are backtesting or paper trading, we need to simulate what would have happened at the exchange.
|
|
*/
|
|
const MODULE_NAME = 'Orders Simulations'
|
|
|
|
let thisObject = {
|
|
actualSizeSimulation: actualSizeSimulation,
|
|
actualRateSimulation: actualRateSimulation,
|
|
feesToBePaidSimulation: feesToBePaidSimulation,
|
|
amountReceivedSimulation: amountReceivedSimulation,
|
|
percentageFilledSimulation: percentageFilledSimulation,
|
|
feesPaidSimulation: feesPaidSimulation,
|
|
sizeFilledSimulation: sizeFilledSimulation,
|
|
initialize: initialize,
|
|
finalize: finalize
|
|
}
|
|
|
|
let tradingEngine
|
|
let tradingSystem
|
|
let sessionParameters
|
|
|
|
return thisObject
|
|
|
|
function initialize() {
|
|
sessionParameters = TS.projects.superalgos.globals.processConstants.CONSTANTS_BY_PROCESS_INDEX_MAP.get(processIndex).TRADING_SESSION_NODE.tradingParameters
|
|
tradingEngine = bot.simulationState.tradingEngine
|
|
tradingSystem = bot.simulationState.tradingSystem
|
|
}
|
|
|
|
function finalize() {
|
|
sessionParameters = undefined
|
|
tradingEngine = undefined
|
|
tradingSystem = undefined
|
|
}
|
|
|
|
async function actualSizeSimulation(tradingEngineStage, tradingSystemOrder, tradingEngineOrder, applyFeePercentage) {
|
|
/*
|
|
The Actual Size is the size the exchange accepts for the order,
|
|
regarless of the size sent in the request to the exchange.
|
|
|
|
While backtesting and paper trading, we do not really need to
|
|
simulate an Actual Size. We will just copy to that field the
|
|
size of the order.
|
|
|
|
This calculation needs to happen only once, so as to set these values.
|
|
*/
|
|
if (tradingEngineOrder.orderBaseAsset.actualSize.value !== tradingEngineOrder.orderBaseAsset.actualSize.config.initialValue) { return }
|
|
|
|
tradingEngineOrder.orderBaseAsset.actualSize.value = tradingEngineOrder.orderBaseAsset.size.value
|
|
tradingEngineOrder.orderQuotedAsset.actualSize.value = tradingEngineOrder.orderQuotedAsset.size.value
|
|
}
|
|
|
|
function actualRateSimulation(tradingEngineStage, tradingSystemOrder, tradingEngineOrder, applyFeePercentage) {
|
|
/* Actual Rate Simulation */
|
|
let previousQuotedAssetActualSize = tradingEngineOrder.orderQuotedAsset.actualSize.value
|
|
|
|
if (tradingEngineOrder.orderStatistics.actualRate.value === tradingEngineOrder.orderStatistics.actualRate.config.initialValue) {
|
|
/*
|
|
The default value for the Actual Rate is the rate of the order.
|
|
*/
|
|
tradingEngineOrder.orderStatistics.actualRate.value = tradingEngineOrder.rate.value
|
|
}
|
|
|
|
basedOnSessionParameters()
|
|
basedOnTradingSystem()
|
|
considerBestMatchWithOrderBook()
|
|
recalculateActualSize()
|
|
recalculateSizePlaced()
|
|
|
|
function basedOnSessionParameters() {
|
|
/*
|
|
The default way to calculate the Actual Rate is to apply the Slippage defined
|
|
at the Session Parameters Definition. We are not going to apply the default if there
|
|
is a definition for this order on how to calculate the actual rate.
|
|
*/
|
|
if (tradingSystemOrder.simulatedExchangeEvents !== undefined) {
|
|
if (tradingSystemOrder.simulatedExchangeEvents.simulatedActualRate !== undefined) {
|
|
return
|
|
}
|
|
}
|
|
switch (tradingEngineOrder.type) {
|
|
case 'Market Order': {
|
|
|
|
/* Actual Rate is simulated based on the Session Paremeters */
|
|
let slippageAmount = tradingEngineOrder.rate.value * sessionParameters.slippage.config.marketOrderRate / 100
|
|
if (slippageAmount !== 0) {
|
|
switch (tradingSystemOrder.type) {
|
|
case 'Market Sell Order': {
|
|
tradingEngineOrder.orderStatistics.actualRate.value = tradingEngineOrder.rate.value - slippageAmount
|
|
tradingEngineOrder.orderStatistics.actualRate.value = TS.projects.superalgos.utilities.miscellaneousFunctions.truncateToThisPrecision(tradingEngineOrder.orderStatistics.actualRate.value, 10)
|
|
|
|
tradingSystem.warnings.push(
|
|
[
|
|
[sessionParameters.slippage.id, tradingEngineOrder.rate.id, tradingEngineOrder.orderStatistics.actualRate.id],
|
|
'slippage.config.marketOrderRate Slippage Amount (' + slippageAmount + ') substracted from Order Rate (' + tradingEngineOrder.rate.value + ') to get the Actual Rate (' + tradingEngineOrder.orderStatistics.actualRate.value + ')'
|
|
]
|
|
)
|
|
break
|
|
}
|
|
case 'Market Buy Order': {
|
|
tradingEngineOrder.orderStatistics.actualRate.value = tradingEngineOrder.rate.value + slippageAmount
|
|
tradingEngineOrder.orderStatistics.actualRate.value = TS.projects.superalgos.utilities.miscellaneousFunctions.truncateToThisPrecision(tradingEngineOrder.orderStatistics.actualRate.value, 10)
|
|
|
|
tradingSystem.warnings.push(
|
|
[
|
|
[sessionParameters.slippage.id, tradingEngineOrder.rate.id, tradingEngineOrder.orderStatistics.actualRate.id],
|
|
'slippage.config.marketOrderRate Slippage Amount (' + slippageAmount + ') added to Order Rate (' + tradingEngineOrder.rate.value + ') to get the Actual Rate (' + tradingEngineOrder.orderStatistics.actualRate.value + ')'
|
|
]
|
|
)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
break
|
|
}
|
|
case 'Limit Order': {
|
|
/* In Limit Orders the actual rate is the rate of the order, there is no slippage */
|
|
tradingEngineOrder.orderStatistics.actualRate.value = tradingEngineOrder.rate.value
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
function basedOnTradingSystem() {
|
|
/*
|
|
If the user specified a Formula to simulate the Actual Rate for this order,
|
|
we use that formula here. Note that if this Formula exists, this will override
|
|
the calculation based on Slippage.
|
|
*/
|
|
if (tradingSystemOrder.simulatedExchangeEvents !== undefined) {
|
|
if (tradingSystemOrder.simulatedExchangeEvents.simulatedActualRate !== undefined) {
|
|
if (tradingSystemOrder.simulatedExchangeEvents.simulatedActualRate.formula !== undefined) {
|
|
/* Calculate this only once for this order */
|
|
if (tradingEngineOrder.orderStatistics.actualRate.value === tradingEngineOrder.orderStatistics.actualRate.config.initialValue) {
|
|
let newValue = tradingSystem.formulas.get(tradingSystemOrder.simulatedExchangeEvents.simulatedActualRate.formula.id)
|
|
newValue = TS.projects.superalgos.utilities.miscellaneousFunctions.truncateToThisPrecision(newValue, 10)
|
|
|
|
tradingSystem.warnings.push(
|
|
[
|
|
tradingSystemOrder.simulatedExchangeEvents.simulatedActualRate.formula.id,
|
|
'Actual Rate (' + tradingEngineOrder.orderStatistics.actualRate.value + ') calculated with this Formula (' + newValue + ')'
|
|
]
|
|
)
|
|
tradingEngineOrder.orderStatistics.actualRate.value = newValue
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function considerBestMatchWithOrderBook() {
|
|
/*
|
|
Until here we have got the Actual Rate based on the formula definition or based on session parameters slippage.
|
|
The last check is about watching what happened in the market. Let's remember that the exchange will
|
|
fill the order with the best possible matches at it's order book. That means that if the rate
|
|
we set for the order was too low (for a sale order) or too hight (for a buy order), the actual rate
|
|
should be better than expected.
|
|
|
|
We don't know what it is at the order book, but whe know that the last candle includes trades that bounced between
|
|
the min an max of the candle. That could tell us the above the max and below the min, the order would be matched
|
|
with orders at the order book. For that reason, we do this:
|
|
*/
|
|
switch (true) {
|
|
case tradingSystemOrder.type === 'Market Buy Order' || tradingSystemOrder.type === 'Limit Buy Order': {
|
|
if (tradingEngineOrder.orderStatistics.actualRate.value > tradingEngine.current.episode.candle.max.value) {
|
|
tradingSystem.warnings.push(
|
|
[
|
|
[tradingEngineOrder.orderStatistics.actualRate.id, tradingEngine.current.episode.candle.max.id],
|
|
'Actual Rate (' + tradingEngineOrder.orderStatistics.actualRate.value + ') too high. Changed to candle.max (' + tradingEngine.current.episode.candle.max.value + ')'
|
|
]
|
|
)
|
|
tradingEngineOrder.orderStatistics.actualRate.value = tradingEngine.current.episode.candle.max.value
|
|
}
|
|
break
|
|
}
|
|
case tradingSystemOrder.type === 'Market Sell Order' || tradingSystemOrder.type === 'Limit Sell Order': {
|
|
if (tradingEngineOrder.orderStatistics.actualRate.value < tradingEngine.current.episode.candle.min.value) {
|
|
tradingSystem.warnings.push(
|
|
[
|
|
[tradingEngineOrder.orderStatistics.actualRate.id, tradingEngine.current.episode.candle.min.id],
|
|
'Actual Rate (' + tradingEngineOrder.orderStatistics.actualRate.value + ') too low. Changed to candle.min (' + tradingEngine.current.episode.candle.max.value + ')'
|
|
]
|
|
)
|
|
tradingEngineOrder.orderStatistics.actualRate.value = tradingEngine.current.episode.candle.min.value
|
|
}
|
|
break
|
|
}
|
|
}
|
|
tradingEngineOrder.orderStatistics.actualRate.value = TS.projects.superalgos.utilities.miscellaneousFunctions.truncateToThisPrecision(tradingEngineOrder.orderStatistics.actualRate.value, 10)
|
|
}
|
|
|
|
function recalculateActualSize() {
|
|
if (tradingEngineOrder.orderStatistics.actualRate.value === tradingEngineOrder.rate.value) { return }
|
|
/*
|
|
Now that we have an Actual Rate, we need to adjust the Actual Size in Quoted Asset, since
|
|
Base Asset is what the exchange accepts as input and give information at. Quotes Asset properties are always calculated.
|
|
*/
|
|
tradingEngineOrder.orderQuotedAsset.actualSize.value =
|
|
tradingEngineOrder.orderBaseAsset.actualSize.value *
|
|
tradingEngineOrder.orderStatistics.actualRate.value
|
|
|
|
tradingEngineOrder.orderQuotedAsset.actualSize.value = TS.projects.superalgos.utilities.miscellaneousFunctions.truncateToThisPrecision(tradingEngineOrder.orderQuotedAsset.actualSize.value, 10)
|
|
|
|
tradingSystem.warnings.push(
|
|
[
|
|
[tradingEngineOrder.orderQuotedAsset.actualSize.id, tradingEngineOrder.orderStatistics.actualRate.id],
|
|
'Actual Size (' + previousQuotedAssetActualSize + ') recalculated (' + tradingEngineOrder.orderQuotedAsset.actualSize.value + ') because the Actual Rate (' + tradingEngineOrder.orderStatistics.actualRate.value + ') is different than the Order Rate (' + tradingEngineOrder.rate.value + '))'
|
|
]
|
|
)
|
|
}
|
|
|
|
function recalculateSizePlaced() {
|
|
if (tradingEngineOrder.orderStatistics.actualRate.value === tradingEngineOrder.rate.value) { return }
|
|
/*
|
|
Since the Actual Rate might have changed, we need to recalculate the Size Placed, where we accumulate all the Size Placed of
|
|
all orders of a Stage. For Base Asset there is nothing to do, since the Actual Rate does not have an inpact on it.
|
|
|
|
For Quoted Asset, we need to first unaccount what this same orded added before to Size Placed (with the precious Actual Size) and
|
|
account with the new Actual Size.
|
|
*/
|
|
let previousStageQuotedAssetSizePlaced = tradingEngineStage.stageQuotedAsset.sizePlaced.value
|
|
|
|
tradingEngineStage.stageQuotedAsset.sizePlaced.value =
|
|
tradingEngineStage.stageQuotedAsset.sizePlaced.value -
|
|
previousQuotedAssetActualSize
|
|
|
|
tradingEngineStage.stageQuotedAsset.sizePlaced.value =
|
|
tradingEngineStage.stageQuotedAsset.sizePlaced.value +
|
|
tradingEngineOrder.orderQuotedAsset.actualSize.value
|
|
|
|
tradingEngineStage.stageQuotedAsset.sizePlaced.value = TS.projects.superalgos.utilities.miscellaneousFunctions.truncateToThisPrecision(tradingEngineStage.stageQuotedAsset.sizePlaced.value, 10)
|
|
|
|
tradingSystem.warnings.push(
|
|
[
|
|
[tradingEngineStage.stageQuotedAsset.sizePlaced.id, tradingEngineOrder.orderStatistics.actualRate.id],
|
|
'Size Placed (' + previousStageQuotedAssetSizePlaced + ') recalculated (' + tradingEngineStage.stageQuotedAsset.sizePlaced.value + ') because the Actual Rate (' + tradingEngineOrder.orderStatistics.actualRate.value + ') is different than the Order Rate (' + tradingEngineOrder.rate.value + '))'
|
|
]
|
|
)
|
|
}
|
|
}
|
|
|
|
async function feesToBePaidSimulation(tradingEngineStage, tradingSystemOrder, tradingEngineOrder, applyFeePercentage) {
|
|
/*
|
|
The Fees To Be Paid are the fees that will be paid once the order is 100% Filled.
|
|
*/
|
|
switch (tradingEngineOrder.type) {
|
|
case 'Market Order': {
|
|
await applyFeePercentage(
|
|
'feesToBePaid',
|
|
tradingEngineOrder,
|
|
tradingSystemOrder,
|
|
sessionParameters.feeStructure.config.taker,
|
|
100
|
|
)
|
|
break
|
|
}
|
|
case 'Limit Order': {
|
|
await applyFeePercentage(
|
|
'feesToBePaid',
|
|
tradingEngineOrder,
|
|
tradingSystemOrder,
|
|
sessionParameters.feeStructure.config.maker,
|
|
100
|
|
)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
function percentageFilledSimulation(tradingEngineStage, tradingSystemOrder, tradingEngineOrder, applyFeePercentage) {
|
|
/*
|
|
When we are live, it is the exchange who knows which order was filled and in
|
|
which amount. Since we are backtesting here, we need to simulate what happens
|
|
at the exchange. We can not simulate that an order is filled if the candle we
|
|
see did not touch the order.
|
|
|
|
For limit buy orders we are going to check if the price was below the order actual rate, and
|
|
for limit sell orders we are going to check if the price was above the order actual rate.
|
|
|
|
If we have a partial filling defined specifically for this order,
|
|
we are going to use it to estimate how much of the order we will consider filled.
|
|
*/
|
|
let orderWasHit
|
|
switch (tradingSystemOrder.type) {
|
|
case 'Limit Buy Order': {
|
|
if (tradingEngine.current.episode.candle.min.value <= tradingEngineOrder.orderStatistics.actualRate.value) {
|
|
orderWasHit = true
|
|
}
|
|
break
|
|
}
|
|
case 'Limit Sell Order': {
|
|
if (tradingEngine.current.episode.candle.max.value >= tradingEngineOrder.orderStatistics.actualRate.value) {
|
|
orderWasHit = true
|
|
}
|
|
break
|
|
}
|
|
case 'Market Buy Order': {
|
|
orderWasHit = true
|
|
break
|
|
}
|
|
case 'Market Sell Order': {
|
|
orderWasHit = true
|
|
break
|
|
}
|
|
}
|
|
if (orderWasHit === true) {
|
|
/*
|
|
Initially, we will assume that 100% of the order was filled. This might be overwritten
|
|
if there is a configuration for this order that details how much is must be considered
|
|
filled each time we check this.
|
|
*/
|
|
tradingEngineOrder.orderStatistics.percentageFilled.value = 100
|
|
|
|
/* Based on the Trading System Definition */
|
|
if (tradingSystemOrder.simulatedExchangeEvents !== undefined) {
|
|
if (tradingSystemOrder.simulatedExchangeEvents.simulatedPartialFill !== undefined) {
|
|
if (tradingSystemOrder.simulatedExchangeEvents.simulatedPartialFill.config.fillProbability !== undefined) {
|
|
|
|
/* Percentage Filled */
|
|
let percentageFilled = tradingSystemOrder.simulatedExchangeEvents.simulatedPartialFill.config.fillProbability * 100
|
|
if (tradingEngineOrder.orderStatistics.percentageFilled.value + percentageFilled > 100) {
|
|
percentageFilled = 100 - tradingEngineOrder.orderStatistics.percentageFilled.value
|
|
}
|
|
tradingEngineOrder.orderStatistics.percentageFilled.value = tradingEngineOrder.orderStatistics.percentageFilled.value + percentageFilled
|
|
tradingEngineOrder.orderStatistics.percentageFilled.value = TS.projects.superalgos.utilities.miscellaneousFunctions.truncateToThisPrecision(tradingEngineOrder.orderStatistics.percentageFilled.value, 10)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function feesPaidSimulation(tradingEngineStage, tradingSystemOrder, tradingEngineOrder, applyFeePercentage) {
|
|
/*
|
|
The way the Fess Paid are calculated is as follows: From the order Actual Size we apply the % of fee,
|
|
that would give us the total fees for that order. To that we apply the % filled of the orders,
|
|
that give use the final result of the fees paid for the current % filled.
|
|
|
|
But before that, we will see if there is an specific definition on how to simulate the fees for
|
|
this order, or if there is not, we use the Fees Structure defined at the Session Parameters.
|
|
*/
|
|
|
|
/* Based on the Trading System Definition */
|
|
if (tradingSystemOrder.simulatedExchangeEvents !== undefined) {
|
|
if (tradingSystemOrder.simulatedExchangeEvents.simulatedFeesPaid !== undefined) {
|
|
if (tradingSystemOrder.simulatedExchangeEvents.simulatedFeesPaid.config.percentage !== undefined) {
|
|
applyFeePercentage(
|
|
'feesPaid',
|
|
tradingEngineOrder,
|
|
tradingSystemOrder,
|
|
tradingSystemOrder.simulatedExchangeEvents.simulatedFeesPaid.config.percentage,
|
|
tradingEngineOrder.orderStatistics.percentageFilled.value
|
|
)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
Another way to simulate the Fees Paid is by using the Session Paremeters configuration for Fees.
|
|
*/
|
|
switch (tradingEngineOrder.type) {
|
|
case 'Market Order': {
|
|
applyFeePercentage(
|
|
'feesPaid',
|
|
tradingEngineOrder,
|
|
tradingSystemOrder,
|
|
sessionParameters.feeStructure.config.taker,
|
|
tradingEngineOrder.orderStatistics.percentageFilled.value
|
|
)
|
|
break
|
|
}
|
|
case 'Limit Order': {
|
|
applyFeePercentage(
|
|
'feesPaid',
|
|
tradingEngineOrder,
|
|
tradingSystemOrder,
|
|
sessionParameters.feeStructure.config.maker,
|
|
tradingEngineOrder.orderStatistics.percentageFilled.value
|
|
)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
function sizeFilledSimulation(tradingEngineStage, tradingSystemOrder, tradingEngineOrder, applyFeePercentage) {
|
|
/*
|
|
Size Filled is the amount of Base Asset or Quoted Asset that at the exchange matches
|
|
another counterparty order. We calculate this using the Percentage Filled we already simulated.
|
|
*/
|
|
|
|
tradingEngineOrder.orderBaseAsset.sizeFilled.value =
|
|
tradingEngineOrder.orderBaseAsset.actualSize.value *
|
|
tradingEngineOrder.orderStatistics.percentageFilled.value / 100
|
|
|
|
tradingEngineOrder.orderQuotedAsset.sizeFilled.value =
|
|
tradingEngineOrder.orderQuotedAsset.actualSize.value *
|
|
tradingEngineOrder.orderStatistics.percentageFilled.value / 100
|
|
|
|
tradingEngineOrder.orderBaseAsset.sizeFilled.value = TS.projects.superalgos.utilities.miscellaneousFunctions.truncateToThisPrecision(tradingEngineOrder.orderBaseAsset.sizeFilled.value, 10)
|
|
tradingEngineOrder.orderQuotedAsset.sizeFilled.value = TS.projects.superalgos.utilities.miscellaneousFunctions.truncateToThisPrecision(tradingEngineOrder.orderQuotedAsset.sizeFilled.value, 10)
|
|
}
|
|
|
|
async function amountReceivedSimulation(tradingEngineStage, tradingSystemOrder, tradingEngineOrder, order, applyFeePercentage) {
|
|
/*
|
|
This simulation is for informational purposes, so that users do not have to calculate
|
|
it by themselves. The Amount Received out of the trade depends if we are Buying or Selling
|
|
the Base Asset. If we are Buying, then the Amount Receiced will be in Base Asset. If we
|
|
are selling then it will be in Quoted Asset.
|
|
*/
|
|
|
|
switch (true) {
|
|
case tradingSystemOrder.type === 'Market Buy Order' || tradingSystemOrder.type === 'Limit Buy Order': {
|
|
/*
|
|
In this case the Amount Received is in Base Asset.
|
|
*/
|
|
tradingEngineOrder.orderBaseAsset.amountReceived.value =
|
|
tradingEngineOrder.orderBaseAsset.sizeFilled.value -
|
|
tradingEngineOrder.orderBaseAsset.feesPaid.value
|
|
break
|
|
}
|
|
case tradingSystemOrder.type === 'Market Sell Order' || tradingSystemOrder.type === 'Limit Sell Order': {
|
|
/*
|
|
In this case the Amount Received is in Quoted Asset.
|
|
*/
|
|
tradingEngineOrder.orderQuotedAsset.amountReceived.value =
|
|
tradingEngineOrder.orderQuotedAsset.sizeFilled.value -
|
|
tradingEngineOrder.orderQuotedAsset.feesPaid.value
|
|
break
|
|
}
|
|
}
|
|
}
|
|
} |