diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt index 4a3125534..59e9ee971 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt @@ -11,6 +11,7 @@ import exchange.dydx.abacus.calculator.SlippageConstants.STOP_MARKET_ORDER_SLIPP import exchange.dydx.abacus.calculator.SlippageConstants.TAKE_PROFIT_MARKET_ORDER_SLIPPAGE_BUFFER import exchange.dydx.abacus.calculator.SlippageConstants.TAKE_PROFIT_MARKET_ORDER_SLIPPAGE_BUFFER_MAJOR_MARKET import exchange.dydx.abacus.output.input.MarginMode +import exchange.dydx.abacus.output.input.OrderSide import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.utils.MAX_FREE_CROSS_COLLATERAL_BUFFER_PERCENT import exchange.dydx.abacus.utils.MAX_FREE_ISOLATED_COLLATERAL_BUFFER_PERCENT @@ -475,9 +476,10 @@ internal class TradeInputCalculator( isBuying: Boolean, input: String, ): Map? { + val marketId = parser.asString(market?.get("id")) val tradeSize = parser.asNativeMap(trade["size"]) - if (tradeSize != null) { + if (tradeSize != null && marketId != null) { val maxMarketLeverage = maxMarketLeverage(market) val targetLeverage = parser.asDouble(trade["targetLeverage"]) val marginMode = MarginMode.invoke(parser.asString(trade["marginMode"])) ?: MarginMode.Cross @@ -486,14 +488,40 @@ internal class TradeInputCalculator( } else { maxMarketLeverage } - val freeCollateral = parser.asDouble(parser.value(subaccount, "freeCollateral.current")) ?: Numeric.double.ZERO + val tradeSide = OrderSide.invoke(parser.asString(trade["side"])) + val position = parser.asNativeMap(parser.value(subaccount, "openPositions.$marketId")) + val positionNotionalSize = if (position != null) { + parser.asDouble( + parser.value( + position, + "notionalTotal.current", + ), + ) ?: Numeric.double.ZERO + } else { + Numeric.double.ZERO + } + val positionSize = if (position != null) { + parser.asDouble( + parser.value( + position, + "size.current", + ), + ) ?: Numeric.double.ZERO + } else { + Numeric.double.ZERO + } + val isTradeSameSide = tradeSide != null && + ((tradeSide == OrderSide.Buy && positionSize >= Numeric.double.ZERO) || (tradeSide == OrderSide.Sell && positionSize <= Numeric.double.ZERO)) + return when (input) { "size.size", "size.percent" -> { val orderbook = orderbook(market, isBuying) calculateMarketOrderFromSize( parser.asDouble(tradeSize["size"]), + positionNotionalSize, + isTradeSameSide, freeCollateral, tradeLeverage, orderbook, @@ -507,6 +535,8 @@ internal class TradeInputCalculator( val orderbook = orderbook(market, isBuying) calculateMarketOrderFromUsdcSize( parser.asDouble(tradeSize["usdcSize"]), + positionNotionalSize, + isTradeSameSide, freeCollateral, tradeLeverage, orderbook, @@ -519,6 +549,9 @@ internal class TradeInputCalculator( parser.asDouble(parser.value(trade, "size.leverage")) ?: return null calculateMarketOrderFromLeverage( leverage, + positionNotionalSize, + positionSize, + isTradeSameSide, market, freeCollateral, tradeLeverage, @@ -534,14 +567,21 @@ internal class TradeInputCalculator( val orderbook = orderbook(market, isBuying) val balancePercent = parser.asDouble(parser.value(trade, "size.balancePercent")) ?: return null + val oraclePrice = parser.asDouble(parser.value(market, "oraclePrice")) + val isReduceOnly = parser.asBool(trade.get("reduceOnly")) ?: false calculateMarketOrderFromBalancePercent( balancePercent, + positionNotionalSize, + positionSize, + isTradeSameSide, marginMode, freeCollateral, tradeLeverage, orderbook, stepSize, + oraclePrice, + isReduceOnly, ) } @@ -551,23 +591,56 @@ internal class TradeInputCalculator( return null } + private fun isolatedPnlImpact(marginMode: MarginMode, size: Double, entryPrice: Double, oraclePrice: Double?, isReduceOnly: Boolean): Double { + // Calculate the difference between the oracle price and the ask/bid price in order to determine immediate PnL impact that would affect collateral checks + // Should only apply to orders that are increasing in position size (not reduceOnly) + // In a cleaner world, this would call MarginCalculator.getShouldTransferInCollateralDeprecated and MarginCalculator.getTransferAmountFromTargetLeverage but because it will be deprecated soon anyways, just passing in the necessary variables + return when (marginMode) { + MarginMode.Cross -> Numeric.double.ZERO + MarginMode.Isolated -> if (isReduceOnly) Numeric.double.ZERO else (entryPrice - (oraclePrice ?: entryPrice)).abs() * size + } + } + private fun calculateMarketOrderFromBalancePercent( balancePercent: Double, + existingPositionNotionalSize: Double, + existingPositionSize: Double, + isTradeSameSide: Boolean, marginMode: MarginMode, freeCollateral: Double, tradeLeverage: Double, orderbook: List>?, stepSize: Double, + oraclePrice: Double?, + isReduceOnly: Boolean, ): Map? { - val buffer = when (marginMode) { + if (marginMode == MarginMode.Isolated && !isTradeSameSide) { + // For isolated margin orders where the user is trading on the opposite side of their currentPosition, the balancePercent represents a percentage of their current position rather than freeCollateral + val desiredSize = existingPositionSize.abs() * balancePercent + return calculateMarketOrderFromSize(desiredSize, existingPositionNotionalSize, isTradeSameSide, freeCollateral, tradeLeverage, orderbook) + } + + if (tradeLeverage <= Numeric.double.ZERO) { + return null + } + + val maxPercent = when (marginMode) { MarginMode.Cross -> MAX_FREE_CROSS_COLLATERAL_BUFFER_PERCENT MarginMode.Isolated -> MAX_FREE_ISOLATED_COLLATERAL_BUFFER_PERCENT } - val cappedPercent = min(balancePercent, buffer) - val desiredBalance = cappedPercent * freeCollateral - val usdcSize = desiredBalance * tradeLeverage + val cappedPercent = min(balancePercent, maxPercent) + + val existingBalance = existingPositionNotionalSize.abs() / tradeLeverage + val desiredBalance = when (marginMode) { + MarginMode.Cross -> { + if (isTradeSameSide) cappedPercent * freeCollateral else cappedPercent * (existingBalance + freeCollateral) + existingBalance + } + MarginMode.Isolated -> { + cappedPercent * freeCollateral + } + } - return if (usdcSize != Numeric.double.ZERO) { + return if (desiredBalance != Numeric.double.ZERO) { if (orderbook != null) { var sizeTotal = Numeric.double.ZERO var usdcSizeTotal = Numeric.double.ZERO @@ -584,14 +657,14 @@ internal class TradeInputCalculator( if (entryPrice != null && entryPrice > Numeric.double.ZERO && entrySize != null) { val entryUsdcSize = entrySize * entryPrice val entryBalanceSize = entryUsdcSize / tradeLeverage - filled = (balanceTotal + entryBalanceSize >= desiredBalance) + filled = (balanceTotal + entryBalanceSize + isolatedPnlImpact(marginMode, entrySize, entryPrice, oraclePrice, isReduceOnly)) >= desiredBalance var matchedSize = entrySize var matchedUsdcSize = entryUsdcSize - var matchedBalance = matchedUsdcSize / tradeLeverage + var matchedBalance = matchedUsdcSize / tradeLeverage + isolatedPnlImpact(marginMode, matchedSize, entryPrice, oraclePrice, isReduceOnly) if (filled) { - matchedBalance = desiredBalance - balanceTotal + matchedBalance = desiredBalance - balanceTotal - isolatedPnlImpact(marginMode, (desiredBalance - balanceTotal) * tradeLeverage / entryPrice, entryPrice, oraclePrice, isReduceOnly) matchedUsdcSize = matchedBalance * tradeLeverage matchedSize = matchedUsdcSize / entryPrice matchedSize = @@ -600,6 +673,7 @@ internal class TradeInputCalculator( stepSize, ) matchedUsdcSize = matchedSize * entryPrice + matchedBalance = matchedUsdcSize / tradeLeverage + isolatedPnlImpact(marginMode, matchedSize, entryPrice, oraclePrice, isReduceOnly) } sizeTotal += matchedSize usdcSizeTotal += matchedUsdcSize @@ -622,6 +696,7 @@ internal class TradeInputCalculator( } else { Numeric.double.ZERO } + marketOrder( marketOrderOrderBook, sizeTotal, @@ -647,6 +722,9 @@ internal class TradeInputCalculator( private fun calculateMarketOrderFromLeverage( leverage: Double, + existingPositionNotionalSize: Double, + existingPositionSize: Double, + isTradeSameSide: Boolean, market: Map?, freeCollateral: Double, tradeLeverage: Double, @@ -662,20 +740,9 @@ internal class TradeInputCalculator( val feeRate = parser.asDouble(parser.value(user, "takerFeeRate")) ?: Numeric.double.ZERO - val positions = parser.asNativeMap(subaccount?.get("openPositions")) - val positionSize = if (positions != null && market != null) { - parser.asDouble( - parser.value( - positions, - "${market["id"]}.size.current", - ), - ) - } else { - null - } return if (equity > Numeric.double.ZERO) { val existingLeverage = - ((positionSize ?: Numeric.double.ZERO) * oraclePrice) / equity + (existingPositionSize * oraclePrice) / equity val calculatedIsBuying = if (leverage > existingLeverage) { true @@ -690,11 +757,13 @@ internal class TradeInputCalculator( calculateMarketOrderFromLeverage( equity, oraclePrice, - positionSize, + existingPositionSize, calculatedIsBuying, feeRate, leverage, stepSize, + existingPositionNotionalSize, + isTradeSameSide, freeCollateral, tradeLeverage, orderbook, @@ -712,6 +781,8 @@ internal class TradeInputCalculator( private fun calculateMarketOrderFromSize( size: Double?, + existingPositionNotionalSize: Double, + isTradeSameSide: Boolean, freeCollateral: Double, tradeLeverage: Double, orderbook: List>?, @@ -746,11 +817,7 @@ internal class TradeInputCalculator( } } } - val balancePercentTotal = if (freeCollateral > Numeric.double.ZERO) { - (usdcSizeTotal / tradeLeverage) / freeCollateral - } else { - Numeric.double.ZERO - } + val balancePercentTotal = calculateBalancePercentFromUsdcSize(usdcSize = usdcSizeTotal, freeCollateral = freeCollateral, positionSize = existingPositionNotionalSize, tradeLeverage = tradeLeverage, isTradeSameSide = isTradeSameSide) marketOrder( marketOrderOrderBook, sizeTotal, @@ -801,6 +868,8 @@ internal class TradeInputCalculator( private fun calculateMarketOrderFromUsdcSize( usdcSize: Double?, + existingPositionNotionalSize: Double, + isTradeSameSide: Boolean, freeCollateral: Double, tradeLeverage: Double, orderbook: List>?, @@ -852,11 +921,7 @@ internal class TradeInputCalculator( } } } - val balancePercentTotal = if (freeCollateral > Numeric.double.ZERO) { - (usdcSizeTotal / tradeLeverage) / freeCollateral - } else { - Numeric.double.ZERO - } + val balancePercentTotal = calculateBalancePercentFromUsdcSize(usdcSize = usdcSizeTotal, freeCollateral = freeCollateral, positionSize = existingPositionNotionalSize, tradeLeverage = tradeLeverage, isTradeSameSide = isTradeSameSide) marketOrder( marketOrderOrderBook, sizeTotal, @@ -907,6 +972,8 @@ internal class TradeInputCalculator( feeRate: Double, leverage: Double, stepSize: Double, + existingPositionNotionalSize: Double, + isTradeSameSide: Boolean, freeCollateral: Double, tradeLeverage: Double, orderbook: List>, @@ -1012,11 +1079,7 @@ internal class TradeInputCalculator( break@orderbookLoop } } - val balancePercentTotal = if (freeCollateral > Numeric.double.ZERO) { - (usdcSizeTotal / tradeLeverage) / freeCollateral - } else { - Numeric.double.ZERO - } + val balancePercentTotal = calculateBalancePercentFromUsdcSize(usdcSize = usdcSizeTotal, freeCollateral = freeCollateral, positionSize = existingPositionNotionalSize, tradeLeverage = tradeLeverage, isTradeSameSide = isTradeSameSide) return marketOrder( marketOrderOrderBook, sizeTotal, @@ -1027,6 +1090,24 @@ internal class TradeInputCalculator( ) } + private fun calculateBalancePercentFromUsdcSize( + usdcSize: Double, + freeCollateral: Double, + positionSize: Double, + tradeLeverage: Double, + isTradeSameSide: Boolean, + ): Double { + if (freeCollateral <= Numeric.double.ZERO || tradeLeverage <= Numeric.double.ZERO) { + return Numeric.double.ZERO + } + return if (isTradeSameSide) { + (usdcSize / tradeLeverage) / freeCollateral + } else { + val existingBalance = positionSize.abs() / tradeLeverage + (usdcSize / tradeLeverage - existingBalance) / (freeCollateral + existingBalance) + } + } + private fun side(marketOrder: Map, orderbook: Map): String? { val firstMarketOrderbookPrice = parser.asDouble(parser.value(marketOrder, "orderbook.0.price")) ?: return null diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputMarketOrderCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputMarketOrderCalculator.kt index 75ef78c81..465e2c5a5 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputMarketOrderCalculator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputMarketOrderCalculator.kt @@ -105,6 +105,7 @@ internal class TradeInputMarketOrderCalculator() { user: InternalUserState?, input: String?, ): TradeInputMarketOrder? { + val tradeSide = trade.side val tradeSize = trade.size val freeCollateral = subaccount?.calculated?.get(CalculationPeriod.current)?.freeCollateral @@ -118,11 +119,28 @@ internal class TradeInputMarketOrderCalculator() { maxMarketLeverage } + val positions = subaccount.openPositions + val marketId = market?.perpetualMarket?.id + val positionNotionalSize = if (positions != null && marketId != null) { + positions[marketId]?.calculated?.get(CalculationPeriod.current)?.notionalTotal ?: Numeric.double.ZERO + } else { + Numeric.double.ZERO + } + val positionSize = if (positions != null && marketId != null) { + positions[marketId]?.calculated?.get(CalculationPeriod.current)?.size ?: Numeric.double.ZERO + } else { + Numeric.double.ZERO + } + val isTradeSameSide = tradeSide != null && + ((tradeSide == OrderSide.Buy && positionSize >= Numeric.double.ZERO) || (tradeSide == OrderSide.Sell && positionSize <= Numeric.double.ZERO)) + return when (input) { "size.size", "size.percent" -> { val orderbook = getOrderbook(market = market, isBuying = trade.isBuying) createMarketOrderFromSize( size = tradeSize.size, + existingPositionNotionalSize = positionNotionalSize, + isTradeSameSide = isTradeSameSide, freeCollateral = freeCollateral, tradeLeverage = tradeLeverage, orderbook = orderbook, @@ -134,6 +152,8 @@ internal class TradeInputMarketOrderCalculator() { val orderbook = getOrderbook(market = market, isBuying = trade.isBuying) createMarketOrderFromUsdcSize( usdcSize = tradeSize.usdcSize, + existingPositionNotionalSize = positionNotionalSize, + isTradeSameSide = isTradeSameSide, freeCollateral = freeCollateral, tradeLeverage = tradeLeverage, orderbook = orderbook, @@ -145,6 +165,9 @@ internal class TradeInputMarketOrderCalculator() { val leverage = tradeSize.leverage ?: return null createMarketOrderFromLeverage( leverage = leverage, + existingPositionNotionalSize = positionNotionalSize, + existingPositionSize = positionSize, + isTradeSameSide = isTradeSameSide, market = market, freeCollateral = freeCollateral, tradeLeverage = tradeLeverage, @@ -157,14 +180,21 @@ internal class TradeInputMarketOrderCalculator() { val stepSize = market?.perpetualMarket?.configs?.stepSize ?: 0.001 val orderbook = getOrderbook(market = market, isBuying = trade.isBuying) val balancePercent = tradeSize.balancePercent ?: return null + val oraclePrice = market?.perpetualMarket?.oraclePrice + val isReduceOnly = trade.reduceOnly createMarketOrderFromBalancePercent( balancePercent = balancePercent, + existingPositionNotionalSize = positionNotionalSize, + existingPositionSize = positionSize, + isTradeSameSide = isTradeSameSide, marginMode = marginMode, freeCollateral = freeCollateral, tradeLeverage = tradeLeverage, orderbook = orderbook, stepSize = stepSize, + oraclePrice = oraclePrice, + isReduceOnly = isReduceOnly, ) } @@ -185,23 +215,52 @@ internal class TradeInputMarketOrderCalculator() { } } + private fun isolatedPnlImpact(marginMode: MarginMode, size: Double, entryPrice: Double, oraclePrice: Double?, isReduceOnly: Boolean): Double { + // Calculate the difference between the oracle price and the ask/bid price in order to determine immediate PnL impact that would affect collateral checks + // TODO CT-1192: refactor to call into MarginCalculator.getShouldTransferInCollateralDeprecated and MarginCalculator.getTransferAmountFromTargetLeverage + return when (marginMode) { + MarginMode.Cross -> Numeric.double.ZERO + MarginMode.Isolated -> if (isReduceOnly) Numeric.double.ZERO else (entryPrice - (oraclePrice ?: entryPrice)).abs() * size + } + } + private fun createMarketOrderFromBalancePercent( balancePercent: Double, + existingPositionNotionalSize: Double, + existingPositionSize: Double, + isTradeSameSide: Boolean, marginMode: MarginMode, freeCollateral: Double, tradeLeverage: Double, orderbook: List?, stepSize: Double, + oraclePrice: Double?, + isReduceOnly: Boolean, ): TradeInputMarketOrder? { - val buffer = when (marginMode) { + if (marginMode == MarginMode.Isolated && !isTradeSameSide) { + // For isolated margin orders where the user is trading on the opposite side of their currentPosition, the balancePercent represents a percentage of their current position rather than freeCollateral + val desiredSize = existingPositionSize.abs() * balancePercent + return createMarketOrderFromSize(size = desiredSize, existingPositionNotionalSize = existingPositionNotionalSize, isTradeSameSide = isTradeSameSide, freeCollateral = freeCollateral, tradeLeverage = tradeLeverage, orderbook = orderbook) + } + + val maxPercent = when (marginMode) { MarginMode.Cross -> MAX_FREE_CROSS_COLLATERAL_BUFFER_PERCENT MarginMode.Isolated -> MAX_FREE_ISOLATED_COLLATERAL_BUFFER_PERCENT } - val cappedPercent = min(balancePercent, buffer) - val desiredBalance = cappedPercent * freeCollateral - val usdcSize = desiredBalance * tradeLeverage + val cappedPercent = min(balancePercent, maxPercent) - return if (usdcSize != Numeric.double.ZERO) { + val existingBalance = existingPositionNotionalSize.abs() / tradeLeverage + + val desiredBalance = when (marginMode) { + MarginMode.Cross -> { + if (isTradeSameSide) cappedPercent * freeCollateral else cappedPercent * (existingBalance + freeCollateral) + existingBalance + } + MarginMode.Isolated -> { + cappedPercent * freeCollateral + } + } + + return if (desiredBalance != Numeric.double.ZERO) { if (orderbook != null) { var sizeTotal = Numeric.double.ZERO var usdcSizeTotal = Numeric.double.ZERO @@ -217,14 +276,14 @@ internal class TradeInputMarketOrderCalculator() { if (entryPrice > Numeric.double.ZERO) { val entryUsdcSize = entrySize * entryPrice val entryBalanceSize = entryUsdcSize / tradeLeverage - filled = (balanceTotal + entryBalanceSize >= desiredBalance) + filled = (balanceTotal + entryBalanceSize + isolatedPnlImpact(marginMode = marginMode, size = entrySize, entryPrice = entryPrice, oraclePrice = oraclePrice, isReduceOnly = isReduceOnly)) >= desiredBalance var matchedSize = entrySize var matchedUsdcSize = entryUsdcSize - var matchedBalance = matchedUsdcSize / tradeLeverage + var matchedBalance = matchedUsdcSize / tradeLeverage + isolatedPnlImpact(marginMode = marginMode, size = matchedSize, entryPrice = entryPrice, oraclePrice = oraclePrice, isReduceOnly = isReduceOnly) if (filled) { - matchedBalance = desiredBalance - balanceTotal + matchedBalance = desiredBalance - balanceTotal + isolatedPnlImpact(marginMode = marginMode, size = (desiredBalance - balanceTotal) * tradeLeverage / entryPrice, entryPrice = entryPrice, oraclePrice = oraclePrice, isReduceOnly = isReduceOnly) matchedUsdcSize = matchedBalance * tradeLeverage matchedSize = matchedUsdcSize / entryPrice matchedSize = @@ -233,6 +292,7 @@ internal class TradeInputMarketOrderCalculator() { stepSize, ) matchedUsdcSize = matchedSize * entryPrice + matchedBalance = matchedUsdcSize / tradeLeverage + isolatedPnlImpact(marginMode = marginMode, size = matchedSize, entryPrice = entryPrice, oraclePrice = oraclePrice, isReduceOnly = isReduceOnly) } sizeTotal += matchedSize usdcSizeTotal += matchedUsdcSize @@ -271,6 +331,8 @@ internal class TradeInputMarketOrderCalculator() { private fun createMarketOrderFromSize( size: Double?, + existingPositionNotionalSize: Double, + isTradeSameSide: Boolean, freeCollateral: Double, tradeLeverage: Double, orderbook: List?, @@ -301,7 +363,7 @@ internal class TradeInputMarketOrderCalculator() { break@orderbookLoop } } - val balancePercentTotal = (usdcSizeTotal / tradeLeverage) / freeCollateral + val balancePercentTotal = calculateBalancePercentFromUsdcSize(usdcSize = usdcSizeTotal, freeCollateral = freeCollateral, positionSize = existingPositionNotionalSize, tradeLeverage = tradeLeverage, isTradeSameSide = isTradeSameSide) createMarketOrderWith( orderbook = marketOrderOrderBook, size = sizeTotal, @@ -327,6 +389,8 @@ internal class TradeInputMarketOrderCalculator() { private fun createMarketOrderFromUsdcSize( usdcSize: Double?, + existingPositionNotionalSize: Double, + isTradeSameSide: Boolean, freeCollateral: Double, tradeLeverage: Double, orderbook: List?, @@ -372,7 +436,7 @@ internal class TradeInputMarketOrderCalculator() { } } } - val balancePercentTotal = (usdcSizeTotal / tradeLeverage) / freeCollateral + val balancePercentTotal = calculateBalancePercentFromUsdcSize(usdcSize = usdcSizeTotal, freeCollateral = freeCollateral, positionSize = existingPositionNotionalSize, tradeLeverage = tradeLeverage, isTradeSameSide = isTradeSameSide) createMarketOrderWith( orderbook = marketOrderOrderBook, size = sizeTotal, @@ -398,6 +462,9 @@ internal class TradeInputMarketOrderCalculator() { private fun createMarketOrderFromLeverage( leverage: Double, + existingPositionNotionalSize: Double, + existingPositionSize: Double, + isTradeSameSide: Boolean, market: InternalMarketState?, freeCollateral: Double, tradeLeverage: Double, @@ -408,16 +475,9 @@ internal class TradeInputMarketOrderCalculator() { val equity = subaccount?.calculated?.get(CalculationPeriod.current)?.equity ?: return null val oraclePrice = market?.perpetualMarket?.oraclePrice ?: return null val feeRate = user?.takerFeeRate ?: Numeric.double.ZERO - val positions = subaccount.openPositions - val marketId = market.perpetualMarket?.id - val positionSize = if (positions != null && marketId != null) { - positions[marketId]?.calculated?.get(CalculationPeriod.current)?.size - } else { - null - } return if (equity > Numeric.double.ZERO) { val existingLeverage = - ((positionSize ?: Numeric.double.ZERO) * oraclePrice) / equity + (existingPositionSize * oraclePrice) / equity val calculatedIsBuying = if (leverage > existingLeverage) { true @@ -432,11 +492,13 @@ internal class TradeInputMarketOrderCalculator() { createMarketOrderFromLeverageWith( equity = equity, oraclePrice = oraclePrice, - positionSize = positionSize, + positionSize = existingPositionSize, + positionSizeNotional = existingPositionNotionalSize, isBuying = calculatedIsBuying, feeRate = feeRate, leverage = leverage, stepSize = stepSize, + isTradeSameSide = isTradeSameSide, freeCollateral = freeCollateral, tradeLeverage = tradeLeverage, orderbook = orderbook, @@ -455,11 +517,13 @@ internal class TradeInputMarketOrderCalculator() { private fun createMarketOrderFromLeverageWith( equity: Double, oraclePrice: Double, - positionSize: Double?, + positionSize: Double, + positionSizeNotional: Double, isBuying: Boolean, feeRate: Double, leverage: Double, stepSize: Double, + isTradeSameSide: Boolean, freeCollateral: Double, tradeLeverage: Double, orderbook: List, @@ -517,7 +581,7 @@ internal class TradeInputMarketOrderCalculator() { var AE = equity @Suppress("LocalVariableName", "PropertyName", "VariableNaming") - var SZ = positionSize ?: Numeric.double.ZERO + var SZ = positionSize orderbookLoop@ for (element in orderbook) { val entryPrice = element.price @@ -558,7 +622,7 @@ internal class TradeInputMarketOrderCalculator() { break@orderbookLoop } } - val balancePercentTotal = (usdcSizeTotal / tradeLeverage) / freeCollateral + val balancePercentTotal = calculateBalancePercentFromUsdcSize(usdcSize = usdcSizeTotal, freeCollateral = freeCollateral, positionSize = positionSizeNotional, tradeLeverage = tradeLeverage, isTradeSameSide = isTradeSameSide) return createMarketOrderWith( orderbook = marketOrderOrderBook, size = sizeTotal, @@ -569,6 +633,24 @@ internal class TradeInputMarketOrderCalculator() { ) } + private fun calculateBalancePercentFromUsdcSize( + usdcSize: Double, + freeCollateral: Double, + positionSize: Double, + tradeLeverage: Double, + isTradeSameSide: Boolean, + ): Double { + if (freeCollateral <= Numeric.double.ZERO || tradeLeverage <= Numeric.double.ZERO) { + return Numeric.double.ZERO + } + return if (isTradeSameSide) { + (usdcSize / tradeLeverage) / freeCollateral + } else { + val existingBalance = positionSize.abs() / tradeLeverage + (usdcSize / tradeLeverage - existingBalance) / (freeCollateral + existingBalance) + } + } + private fun createMarketOrderWith( orderbook: List, size: Double?, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/utils/Constants.kt b/src/commonMain/kotlin/exchange.dydx.abacus/utils/Constants.kt index 7770b8e85..38ff7e918 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/utils/Constants.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/utils/Constants.kt @@ -12,10 +12,10 @@ internal const val QUANTUM_MULTIPLIER = 1_000_000 internal const val SLIPPAGE_PERCENT = "1" // Trade Constants -internal const val MAX_FREE_CROSS_COLLATERAL_BUFFER_PERCENT = 0.98 +internal const val MAX_FREE_CROSS_COLLATERAL_BUFFER_PERCENT = 0.95 // Isolated Margin Constants -internal const val MAX_FREE_ISOLATED_COLLATERAL_BUFFER_PERCENT = 0.89 +internal const val MAX_FREE_ISOLATED_COLLATERAL_BUFFER_PERCENT = 0.93 internal const val MAX_LEVERAGE_BUFFER_PERCENT = 0.98 internal const val MARGIN_COLLATERALIZATION_CHECK_BUFFER = 0.01;