From cb747174aed772ff083dec8a19c74527cd032562 Mon Sep 17 00:00:00 2001 From: mulan xia Date: Fri, 13 Sep 2024 14:23:14 -0400 Subject: [PATCH 1/9] ok this works, but need to clean up --- .../calculator/TradeInputCalculator.kt | 94 +++++++++++++++++-- .../exchange.dydx.abacus/utils/Constants.kt | 2 +- 2 files changed, 88 insertions(+), 8 deletions(-) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt index 4a3125534..2534fb87d 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 @@ -488,12 +489,47 @@ internal class TradeInputCalculator( } val freeCollateral = parser.asDouble(parser.value(subaccount, "freeCollateral.current")) ?: Numeric.double.ZERO + val positions = parser.asNativeMap(subaccount?.get("openPositions")) + val position = if (positions != null && market != null) { + parser.value(positions, "${market["id"]}") + } else { + null + } + val positionNotionalSize = if (position != null) { + parser.asDouble( + parser.value( + position, + "notionalTotal.current", + ), + ) + } else { + null + } + val positionSize = if (position != null) { + parser.asDouble( + parser.value( + position, + "size.current", + ), + ) + } else { + null + } + val isTradeSameSide = if (position != null) { + val tradeSide = OrderSide.invoke(parser.asString(trade["side"])) + positionSize != null && tradeSide != null && + ((tradeSide == OrderSide.Buy && positionSize >= 0) || (tradeSide == OrderSide.Sell && positionSize <= 0)) + } else { + true + } return when (input) { "size.size", "size.percent" -> { val orderbook = orderbook(market, isBuying) calculateMarketOrderFromSize( parser.asDouble(tradeSize["size"]), + positionNotionalSize ?: Numeric.double.ZERO, + isTradeSameSide, freeCollateral, tradeLeverage, orderbook, @@ -507,6 +543,8 @@ internal class TradeInputCalculator( val orderbook = orderbook(market, isBuying) calculateMarketOrderFromUsdcSize( parser.asDouble(tradeSize["usdcSize"]), + positionNotionalSize ?: Numeric.double.ZERO, + isTradeSameSide, freeCollateral, tradeLeverage, orderbook, @@ -519,6 +557,8 @@ internal class TradeInputCalculator( parser.asDouble(parser.value(trade, "size.leverage")) ?: return null calculateMarketOrderFromLeverage( leverage, + positionNotionalSize ?: Numeric.double.ZERO, + isTradeSameSide, market, freeCollateral, tradeLeverage, @@ -537,6 +577,9 @@ internal class TradeInputCalculator( calculateMarketOrderFromBalancePercent( balancePercent, + positionNotionalSize ?: Numeric.double.ZERO, + positionSize ?: Numeric.double.ZERO, + isTradeSameSide, marginMode, freeCollateral, tradeLeverage, @@ -553,21 +596,38 @@ internal class TradeInputCalculator( private fun calculateMarketOrderFromBalancePercent( balancePercent: Double, + existingPositionNotionalSize: Double, + existingPositionSize: Double, + isTradeSameSide: Boolean, marginMode: MarginMode, freeCollateral: Double, tradeLeverage: Double, orderbook: List>?, - stepSize: Double, + stepSize: Double ): Map? { + 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, existingPositionSize, isTradeSameSide, freeCollateral, tradeLeverage, orderbook) + } + val buffer = 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 - 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 @@ -622,6 +682,7 @@ internal class TradeInputCalculator( } else { Numeric.double.ZERO } + marketOrder( marketOrderOrderBook, sizeTotal, @@ -647,6 +708,8 @@ internal class TradeInputCalculator( private fun calculateMarketOrderFromLeverage( leverage: Double, + existingPositionNotionalSize: Double, + isTradeSameSide: Boolean, market: Map?, freeCollateral: Double, tradeLeverage: Double, @@ -695,6 +758,8 @@ internal class TradeInputCalculator( feeRate, leverage, stepSize, + existingPositionNotionalSize, + isTradeSameSide, freeCollateral, tradeLeverage, orderbook, @@ -712,6 +777,8 @@ internal class TradeInputCalculator( private fun calculateMarketOrderFromSize( size: Double?, + existingPositionNotionalSize: Double, + isTradeSameSide: Boolean, freeCollateral: Double, tradeLeverage: Double, orderbook: List>?, @@ -746,8 +813,11 @@ internal class TradeInputCalculator( } } } - val balancePercentTotal = if (freeCollateral > Numeric.double.ZERO) { + val balancePercentTotal = if (freeCollateral > Numeric.double.ZERO && isTradeSameSide) { (usdcSizeTotal / tradeLeverage) / freeCollateral + } else if (freeCollateral > Numeric.double.ZERO && !isTradeSameSide) { + val existingBalance = existingPositionNotionalSize.abs() / tradeLeverage + (usdcSizeTotal / tradeLeverage - existingBalance) / (freeCollateral + existingBalance) } else { Numeric.double.ZERO } @@ -801,6 +871,8 @@ internal class TradeInputCalculator( private fun calculateMarketOrderFromUsdcSize( usdcSize: Double?, + existingPositionNotionalSize: Double, + isTradeSameSide: Boolean, freeCollateral: Double, tradeLeverage: Double, orderbook: List>?, @@ -852,8 +924,11 @@ internal class TradeInputCalculator( } } } - val balancePercentTotal = if (freeCollateral > Numeric.double.ZERO) { + val balancePercentTotal = if (freeCollateral > Numeric.double.ZERO && isTradeSameSide) { (usdcSizeTotal / tradeLeverage) / freeCollateral + } else if (freeCollateral > Numeric.double.ZERO && !isTradeSameSide) { + val existingBalance = existingPositionNotionalSize.abs() / tradeLeverage + (usdcSizeTotal / tradeLeverage - existingBalance) / (freeCollateral + existingBalance) } else { Numeric.double.ZERO } @@ -907,6 +982,8 @@ internal class TradeInputCalculator( feeRate: Double, leverage: Double, stepSize: Double, + existingPositionNotionalSize: Double, + isTradeSameSide: Boolean, freeCollateral: Double, tradeLeverage: Double, orderbook: List>, @@ -1012,8 +1089,11 @@ internal class TradeInputCalculator( break@orderbookLoop } } - val balancePercentTotal = if (freeCollateral > Numeric.double.ZERO) { + val balancePercentTotal = if (freeCollateral > Numeric.double.ZERO && isTradeSameSide) { (usdcSizeTotal / tradeLeverage) / freeCollateral + } else if (freeCollateral > Numeric.double.ZERO && !isTradeSameSide) { + val existingBalance = existingPositionNotionalSize.abs() / tradeLeverage + (usdcSizeTotal / tradeLeverage - existingBalance) / (freeCollateral + existingBalance) } else { Numeric.double.ZERO } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/utils/Constants.kt b/src/commonMain/kotlin/exchange.dydx.abacus/utils/Constants.kt index 7770b8e85..0acbfcf68 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/utils/Constants.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/utils/Constants.kt @@ -12,7 +12,7 @@ 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 From 2e179de316b99f70839d15868527176acf0cada3 Mon Sep 17 00:00:00 2001 From: mulan xia Date: Fri, 13 Sep 2024 14:42:57 -0400 Subject: [PATCH 2/9] cleaned the file --- .../calculator/TradeInputCalculator.kt | 106 +++++++----------- 1 file changed, 43 insertions(+), 63 deletions(-) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt index 2534fb87d..d216f7daa 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt @@ -476,9 +476,10 @@ internal class TradeInputCalculator( isBuying: Boolean, input: String, ): Map? { + val marketId = parser.asString(trade["marketId"]) 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 @@ -487,23 +488,19 @@ internal class TradeInputCalculator( } else { maxMarketLeverage } - val freeCollateral = parser.asDouble(parser.value(subaccount, "freeCollateral.current")) ?: Numeric.double.ZERO - val positions = parser.asNativeMap(subaccount?.get("openPositions")) - val position = if (positions != null && market != null) { - parser.value(positions, "${market["id"]}") - } else { - null - } + + val tradeSide = OrderSide.invoke(parser.asString(trade["side"])) + val position = parser.asNativeMap(subaccount?.get("openPositions.$marketId")) val positionNotionalSize = if (position != null) { parser.asDouble( parser.value( position, "notionalTotal.current", ), - ) + ) ?: Numeric.double.ZERO } else { - null + Numeric.double.ZERO } val positionSize = if (position != null) { parser.asDouble( @@ -511,24 +508,19 @@ internal class TradeInputCalculator( position, "size.current", ), - ) + ) ?: Numeric.double.ZERO } else { - null - } - val isTradeSameSide = if (position != null) { - val tradeSide = OrderSide.invoke(parser.asString(trade["side"])) - positionSize != null && tradeSide != null && - ((tradeSide == OrderSide.Buy && positionSize >= 0) || (tradeSide == OrderSide.Sell && positionSize <= 0)) - } else { - true + 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 ?: Numeric.double.ZERO, + positionNotionalSize, isTradeSameSide, freeCollateral, tradeLeverage, @@ -543,7 +535,7 @@ internal class TradeInputCalculator( val orderbook = orderbook(market, isBuying) calculateMarketOrderFromUsdcSize( parser.asDouble(tradeSize["usdcSize"]), - positionNotionalSize ?: Numeric.double.ZERO, + positionNotionalSize, isTradeSameSide, freeCollateral, tradeLeverage, @@ -557,7 +549,8 @@ internal class TradeInputCalculator( parser.asDouble(parser.value(trade, "size.leverage")) ?: return null calculateMarketOrderFromLeverage( leverage, - positionNotionalSize ?: Numeric.double.ZERO, + positionNotionalSize, + positionSize, isTradeSameSide, market, freeCollateral, @@ -577,8 +570,8 @@ internal class TradeInputCalculator( calculateMarketOrderFromBalancePercent( balancePercent, - positionNotionalSize ?: Numeric.double.ZERO, - positionSize ?: Numeric.double.ZERO, + positionNotionalSize, + positionSize, isTradeSameSide, marginMode, freeCollateral, @@ -709,6 +702,7 @@ internal class TradeInputCalculator( private fun calculateMarketOrderFromLeverage( leverage: Double, existingPositionNotionalSize: Double, + existingPositionSize: Double, isTradeSameSide: Boolean, market: Map?, freeCollateral: Double, @@ -725,20 +719,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 @@ -753,7 +736,7 @@ internal class TradeInputCalculator( calculateMarketOrderFromLeverage( equity, oraclePrice, - positionSize, + existingPositionSize, calculatedIsBuying, feeRate, leverage, @@ -813,14 +796,7 @@ internal class TradeInputCalculator( } } } - val balancePercentTotal = if (freeCollateral > Numeric.double.ZERO && isTradeSameSide) { - (usdcSizeTotal / tradeLeverage) / freeCollateral - } else if (freeCollateral > Numeric.double.ZERO && !isTradeSameSide) { - val existingBalance = existingPositionNotionalSize.abs() / tradeLeverage - (usdcSizeTotal / tradeLeverage - existingBalance) / (freeCollateral + existingBalance) - } else { - Numeric.double.ZERO - } + val balancePercentTotal = calculateBalancePercentFromUsdcSize(usdcSize = usdcSizeTotal, freeCollateral = freeCollateral, positionSize = existingPositionNotionalSize, tradeLeverage = tradeLeverage, isTradeSameSide = isTradeSameSide) marketOrder( marketOrderOrderBook, sizeTotal, @@ -924,14 +900,7 @@ internal class TradeInputCalculator( } } } - val balancePercentTotal = if (freeCollateral > Numeric.double.ZERO && isTradeSameSide) { - (usdcSizeTotal / tradeLeverage) / freeCollateral - } else if (freeCollateral > Numeric.double.ZERO && !isTradeSameSide) { - val existingBalance = existingPositionNotionalSize.abs() / tradeLeverage - (usdcSizeTotal / tradeLeverage - existingBalance) / (freeCollateral + existingBalance) - } else { - Numeric.double.ZERO - } + val balancePercentTotal = calculateBalancePercentFromUsdcSize(usdcSize = usdcSizeTotal, freeCollateral = freeCollateral, positionSize = existingPositionNotionalSize, tradeLeverage = tradeLeverage, isTradeSameSide = isTradeSameSide) marketOrder( marketOrderOrderBook, sizeTotal, @@ -977,7 +946,7 @@ internal class TradeInputCalculator( private fun calculateMarketOrderFromLeverage( equity: Double, oraclePrice: Double, - positionSize: Double?, + existingPositionSize: Double?, isBuying: Boolean, feeRate: Double, leverage: Double, @@ -1041,7 +1010,7 @@ internal class TradeInputCalculator( var AE = parser.asDouble(equity)!! @Suppress("LocalVariableName", "PropertyName") - var SZ = parser.asDouble(positionSize) ?: Numeric.double.ZERO + var SZ = parser.asDouble(existingPositionSize) ?: Numeric.double.ZERO orderbookLoop@ for (i in 0 until orderbook.size) { val entry = orderbook[i] @@ -1089,14 +1058,7 @@ internal class TradeInputCalculator( break@orderbookLoop } } - val balancePercentTotal = if (freeCollateral > Numeric.double.ZERO && isTradeSameSide) { - (usdcSizeTotal / tradeLeverage) / freeCollateral - } else if (freeCollateral > Numeric.double.ZERO && !isTradeSameSide) { - val existingBalance = existingPositionNotionalSize.abs() / tradeLeverage - (usdcSizeTotal / tradeLeverage - existingBalance) / (freeCollateral + existingBalance) - } else { - Numeric.double.ZERO - } + val balancePercentTotal = calculateBalancePercentFromUsdcSize(usdcSize = usdcSizeTotal, freeCollateral = freeCollateral, positionSize = existingPositionNotionalSize, tradeLeverage = tradeLeverage, isTradeSameSide = isTradeSameSide) return marketOrder( marketOrderOrderBook, sizeTotal, @@ -1107,6 +1069,24 @@ internal class TradeInputCalculator( ) } + private fun calculateBalancePercentFromUsdcSize( + usdcSize: Double, + freeCollateral: Double, + positionSize: Double, + tradeLeverage: Double, + isTradeSameSide: Boolean, + ): Double { + if (freeCollateral <= 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 From 988c5301cb79a206ec4d07fc209ecde145c11d60 Mon Sep 17 00:00:00 2001 From: mulan xia Date: Fri, 13 Sep 2024 14:53:22 -0400 Subject: [PATCH 3/9] oops fix parsing --- .../exchange.dydx.abacus/calculator/TradeInputCalculator.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt index d216f7daa..825c4e90d 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt @@ -491,7 +491,7 @@ internal class TradeInputCalculator( val freeCollateral = parser.asDouble(parser.value(subaccount, "freeCollateral.current")) ?: Numeric.double.ZERO val tradeSide = OrderSide.invoke(parser.asString(trade["side"])) - val position = parser.asNativeMap(subaccount?.get("openPositions.$marketId")) + val position = parser.asNativeMap(parser.value(subaccount, "openPositions.$marketId")) val positionNotionalSize = if (position != null) { parser.asDouble( parser.value( From 24d1ba411eda7de676324960939c5d8eb00d322e Mon Sep 17 00:00:00 2001 From: mulan xia Date: Fri, 13 Sep 2024 15:00:36 -0400 Subject: [PATCH 4/9] fix up percentages for isolated --- .../calculator/TradeInputCalculator.kt | 25 +++++++++++++------ .../exchange.dydx.abacus/utils/Constants.kt | 2 +- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt index 825c4e90d..3d9966283 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt @@ -567,7 +567,7 @@ 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")) ?: Numeric.double.ZERO calculateMarketOrderFromBalancePercent( balancePercent, positionNotionalSize, @@ -578,6 +578,7 @@ internal class TradeInputCalculator( tradeLeverage, orderbook, stepSize, + oraclePrice, ) } @@ -587,6 +588,14 @@ internal class TradeInputCalculator( return null } + private fun isolatedPnlImpact(marginMode: MarginMode, size: Double, entryPrice: Double, oraclePrice: Double): 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 + return when (marginMode) { + MarginMode.Cross -> Numeric.double.ZERO + MarginMode.Isolated -> (entryPrice - oraclePrice).abs() * size + } + } + private fun calculateMarketOrderFromBalancePercent( balancePercent: Double, existingPositionNotionalSize: Double, @@ -596,7 +605,8 @@ internal class TradeInputCalculator( freeCollateral: Double, tradeLeverage: Double, orderbook: List>?, - stepSize: Double + stepSize: Double, + oraclePrice: Double, ): Map? { 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 @@ -604,11 +614,11 @@ internal class TradeInputCalculator( return calculateMarketOrderFromSize(desiredSize, existingPositionSize, isTradeSameSide, freeCollateral, tradeLeverage, orderbook) } - val buffer = when (marginMode) { + 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 cappedPercent = min(balancePercent, maxPercent) val existingBalance = existingPositionNotionalSize.abs() / tradeLeverage val desiredBalance = when (marginMode) { @@ -637,14 +647,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)) >= desiredBalance var matchedSize = entrySize var matchedUsdcSize = entryUsdcSize - var matchedBalance = matchedUsdcSize / tradeLeverage + var matchedBalance = matchedUsdcSize / tradeLeverage + (entryPrice - oraclePrice).abs() * matchedSize if (filled) { - matchedBalance = desiredBalance - balanceTotal + matchedBalance = desiredBalance - balanceTotal - isolatedPnlImpact(marginMode, (desiredBalance - balanceTotal) * tradeLeverage / entryPrice, entryPrice, oraclePrice) matchedUsdcSize = matchedBalance * tradeLeverage matchedSize = matchedUsdcSize / entryPrice matchedSize = @@ -653,6 +663,7 @@ internal class TradeInputCalculator( stepSize, ) matchedUsdcSize = matchedSize * entryPrice + matchedBalance = matchedUsdcSize / tradeLeverage + isolatedPnlImpact(marginMode, matchedSize, entryPrice, oraclePrice) } sizeTotal += matchedSize usdcSizeTotal += matchedUsdcSize diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/utils/Constants.kt b/src/commonMain/kotlin/exchange.dydx.abacus/utils/Constants.kt index 0acbfcf68..38ff7e918 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/utils/Constants.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/utils/Constants.kt @@ -15,7 +15,7 @@ internal const val SLIPPAGE_PERCENT = "1" 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; From 369f5083dcacaedeb4a57f6c11f52101024742ce Mon Sep 17 00:00:00 2001 From: mulan xia Date: Fri, 13 Sep 2024 16:39:31 -0400 Subject: [PATCH 5/9] hm --- .../calculator/TradeInputCalculator.kt | 15 ++- .../TradeInputMarketOrderCalculator.kt | 126 ++++++++++++++---- 2 files changed, 110 insertions(+), 31 deletions(-) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt index 3d9966283..5058b9adb 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt @@ -529,7 +529,7 @@ internal class TradeInputCalculator( } "size.usdcSize" -> { - val stepSize = + val stepSize = parser.asDouble(parser.value(market, "configs.stepSize")) ?: 0.001 val orderbook = orderbook(market, isBuying) @@ -567,7 +567,8 @@ 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")) ?: Numeric.double.ZERO + val oraclePrice = parser.asDouble(parser.value(market, "oraclePrice")) + calculateMarketOrderFromBalancePercent( balancePercent, positionNotionalSize, @@ -588,11 +589,11 @@ internal class TradeInputCalculator( return null } - private fun isolatedPnlImpact(marginMode: MarginMode, size: Double, entryPrice: Double, oraclePrice: Double): Double { + private fun isolatedPnlImpact(marginMode: MarginMode, size: Double, entryPrice: Double, oraclePrice: Double?): 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 return when (marginMode) { MarginMode.Cross -> Numeric.double.ZERO - MarginMode.Isolated -> (entryPrice - oraclePrice).abs() * size + MarginMode.Isolated -> (entryPrice - (oraclePrice ?: entryPrice)).abs() * size } } @@ -606,12 +607,12 @@ internal class TradeInputCalculator( tradeLeverage: Double, orderbook: List>?, stepSize: Double, - oraclePrice: Double, + oraclePrice: Double?, ): Map? { 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, existingPositionSize, isTradeSameSide, freeCollateral, tradeLeverage, orderbook) + return calculateMarketOrderFromSize(desiredSize, existingPositionNotionalSize, isTradeSameSide, freeCollateral, tradeLeverage, orderbook) } val maxPercent = when (marginMode) { @@ -651,7 +652,7 @@ internal class TradeInputCalculator( var matchedSize = entrySize var matchedUsdcSize = entryUsdcSize - var matchedBalance = matchedUsdcSize / tradeLeverage + (entryPrice - oraclePrice).abs() * matchedSize + var matchedBalance = matchedUsdcSize / tradeLeverage + isolatedPnlImpact(marginMode, matchedSize, entryPrice, oraclePrice) if (filled) { matchedBalance = desiredBalance - balanceTotal - isolatedPnlImpact(marginMode, (desiredBalance - balanceTotal) * tradeLeverage / entryPrice, entryPrice, oraclePrice) 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..96f409ed8 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,10 +105,11 @@ internal class TradeInputMarketOrderCalculator() { user: InternalUserState?, input: String?, ): TradeInputMarketOrder? { + val tradeSide = trade.side val tradeSize = trade.size val freeCollateral = subaccount?.calculated?.get(CalculationPeriod.current)?.freeCollateral - if (tradeSize != null && freeCollateral != null && freeCollateral > Numeric.double.ZERO) { + if (tradeSize != null && freeCollateral != null && freeCollateral > Numeric.double.ZERO && market != null) { val maxMarketLeverage = market?.perpetualMarket?.configs?.maxMarketLeverage ?: Numeric.double.ONE val targetLeverage = trade.targetLeverage val marginMode = trade.marginMode ?: MarginMode.Cross @@ -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, @@ -154,17 +177,22 @@ internal class TradeInputMarketOrderCalculator() { } "size.balancePercent" -> { - val stepSize = market?.perpetualMarket?.configs?.stepSize ?: 0.001 + 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 createMarketOrderFromBalancePercent( balancePercent = balancePercent, + existingPositionNotionalSize = positionNotionalSize, + existingPositionSize = positionSize, + isTradeSameSide = isTradeSameSide, marginMode = marginMode, freeCollateral = freeCollateral, tradeLeverage = tradeLeverage, orderbook = orderbook, stepSize = stepSize, + oraclePrice = oraclePrice, ) } @@ -185,23 +213,50 @@ internal class TradeInputMarketOrderCalculator() { } } + private fun isolatedPnlImpact(marginMode: MarginMode, size: Double, entryPrice: Double, oraclePrice: Double?): 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 + return when (marginMode) { + MarginMode.Cross -> Numeric.double.ZERO + MarginMode.Isolated -> (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?, ): 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) + + 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 @@ -217,14 +272,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)) >= desiredBalance var matchedSize = entrySize var matchedUsdcSize = entryUsdcSize - var matchedBalance = matchedUsdcSize / tradeLeverage + var matchedBalance = matchedUsdcSize / tradeLeverage + isolatedPnlImpact(marginMode = marginMode, size = matchedSize, entryPrice = entryPrice, oraclePrice = oraclePrice) if (filled) { - matchedBalance = desiredBalance - balanceTotal + matchedBalance = desiredBalance - balanceTotal + isolatedPnlImpact(marginMode = marginMode, size = (desiredBalance - balanceTotal) * tradeLeverage / entryPrice, entryPrice = entryPrice, oraclePrice = oraclePrice) matchedUsdcSize = matchedBalance * tradeLeverage matchedSize = matchedUsdcSize / entryPrice matchedSize = @@ -233,6 +288,7 @@ internal class TradeInputMarketOrderCalculator() { stepSize, ) matchedUsdcSize = matchedSize * entryPrice + matchedBalance = matchedUsdcSize / tradeLeverage + isolatedPnlImpact(marginMode = marginMode, size = matchedSize, entryPrice = entryPrice, oraclePrice = oraclePrice) } sizeTotal += matchedSize usdcSizeTotal += matchedUsdcSize @@ -271,6 +327,8 @@ internal class TradeInputMarketOrderCalculator() { private fun createMarketOrderFromSize( size: Double?, + existingPositionNotionalSize: Double, + isTradeSameSide: Boolean, freeCollateral: Double, tradeLeverage: Double, orderbook: List?, @@ -301,7 +359,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 +385,8 @@ internal class TradeInputMarketOrderCalculator() { private fun createMarketOrderFromUsdcSize( usdcSize: Double?, + existingPositionNotionalSize: Double, + isTradeSameSide: Boolean, freeCollateral: Double, tradeLeverage: Double, orderbook: List?, @@ -372,7 +432,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 +458,9 @@ internal class TradeInputMarketOrderCalculator() { private fun createMarketOrderFromLeverage( leverage: Double, + existingPositionNotionalSize: Double, + existingPositionSize: Double, + isTradeSameSide: Boolean, market: InternalMarketState?, freeCollateral: Double, tradeLeverage: Double, @@ -408,16 +471,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 +488,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 +513,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 +577,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 +618,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 +629,24 @@ internal class TradeInputMarketOrderCalculator() { ) } + private fun calculateBalancePercentFromUsdcSize( + usdcSize: Double, + freeCollateral: Double, + positionSize: Double, + tradeLeverage: Double, + isTradeSameSide: Boolean, + ): Double { + if (freeCollateral <= 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?, From 9a6cc5fb8aa0ae4c04899c999edbd9d99b1f6933 Mon Sep 17 00:00:00 2001 From: mulan xia Date: Fri, 13 Sep 2024 16:42:31 -0400 Subject: [PATCH 6/9] fix test --- .../calculator/TradeInputCalculator.kt | 6 +++--- .../V2/TradeInput/TradeInputMarketOrderCalculator.kt | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt index 5058b9adb..e4a273ca5 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt @@ -529,7 +529,7 @@ internal class TradeInputCalculator( } "size.usdcSize" -> { - val stepSize = + val stepSize = parser.asDouble(parser.value(market, "configs.stepSize")) ?: 0.001 val orderbook = orderbook(market, isBuying) @@ -958,7 +958,7 @@ internal class TradeInputCalculator( private fun calculateMarketOrderFromLeverage( equity: Double, oraclePrice: Double, - existingPositionSize: Double?, + positionSize: Double?, isBuying: Boolean, feeRate: Double, leverage: Double, @@ -1022,7 +1022,7 @@ internal class TradeInputCalculator( var AE = parser.asDouble(equity)!! @Suppress("LocalVariableName", "PropertyName") - var SZ = parser.asDouble(existingPositionSize) ?: Numeric.double.ZERO + var SZ = parser.asDouble(positionSize) ?: Numeric.double.ZERO orderbookLoop@ for (i in 0 until orderbook.size) { val entry = orderbook[i] 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 96f409ed8..e49fc214a 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 @@ -109,7 +109,7 @@ internal class TradeInputMarketOrderCalculator() { val tradeSize = trade.size val freeCollateral = subaccount?.calculated?.get(CalculationPeriod.current)?.freeCollateral - if (tradeSize != null && freeCollateral != null && freeCollateral > Numeric.double.ZERO && market != null) { + if (tradeSize != null && freeCollateral != null && freeCollateral > Numeric.double.ZERO) { val maxMarketLeverage = market?.perpetualMarket?.configs?.maxMarketLeverage ?: Numeric.double.ONE val targetLeverage = trade.targetLeverage val marginMode = trade.marginMode ?: MarginMode.Cross @@ -120,7 +120,7 @@ internal class TradeInputMarketOrderCalculator() { } val positions = subaccount.openPositions - val marketId = market.perpetualMarket?.id + val marketId = market?.perpetualMarket?.id val positionNotionalSize = if (positions != null && marketId != null) { positions[marketId]?.calculated?.get(CalculationPeriod.current)?.notionalTotal ?: Numeric.double.ZERO } else { @@ -177,10 +177,10 @@ internal class TradeInputMarketOrderCalculator() { } "size.balancePercent" -> { - val stepSize = market.perpetualMarket?.configs?.stepSize ?: 0.001 + 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 oraclePrice = market?.perpetualMarket?.oraclePrice createMarketOrderFromBalancePercent( balancePercent = balancePercent, From a68d69c71eb7abbe15a2adf286498131a4ba429d Mon Sep 17 00:00:00 2001 From: mulan xia Date: Fri, 13 Sep 2024 17:04:42 -0400 Subject: [PATCH 7/9] added TODO --- .../calculator/TradeInputCalculator.kt | 17 +++++++++++------ .../TradeInputMarketOrderCalculator.kt | 16 ++++++++++------ 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt index e4a273ca5..218f192c0 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt @@ -568,6 +568,7 @@ internal class TradeInputCalculator( 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, @@ -580,6 +581,7 @@ internal class TradeInputCalculator( orderbook, stepSize, oraclePrice, + isReduceOnly, ) } @@ -589,11 +591,13 @@ internal class TradeInputCalculator( return null } - private fun isolatedPnlImpact(marginMode: MarginMode, size: Double, entryPrice: Double, oraclePrice: Double?): Double { + 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 -> (entryPrice - (oraclePrice ?: entryPrice)).abs() * size + MarginMode.Isolated -> if (isReduceOnly) Numeric.double.ZERO else (entryPrice - (oraclePrice ?: entryPrice)).abs() * size } } @@ -608,6 +612,7 @@ internal class TradeInputCalculator( orderbook: List>?, stepSize: Double, oraclePrice: Double?, + isReduceOnly: Boolean, ): Map? { 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 @@ -648,14 +653,14 @@ internal class TradeInputCalculator( if (entryPrice != null && entryPrice > Numeric.double.ZERO && entrySize != null) { val entryUsdcSize = entrySize * entryPrice val entryBalanceSize = entryUsdcSize / tradeLeverage - filled = (balanceTotal + entryBalanceSize + isolatedPnlImpact(marginMode, entrySize, entryPrice, oraclePrice)) >= desiredBalance + filled = (balanceTotal + entryBalanceSize + isolatedPnlImpact(marginMode, entrySize, entryPrice, oraclePrice, isReduceOnly)) >= desiredBalance var matchedSize = entrySize var matchedUsdcSize = entryUsdcSize - var matchedBalance = matchedUsdcSize / tradeLeverage + isolatedPnlImpact(marginMode, matchedSize, entryPrice, oraclePrice) + var matchedBalance = matchedUsdcSize / tradeLeverage + isolatedPnlImpact(marginMode, matchedSize, entryPrice, oraclePrice, isReduceOnly) if (filled) { - matchedBalance = desiredBalance - balanceTotal - isolatedPnlImpact(marginMode, (desiredBalance - balanceTotal) * tradeLeverage / entryPrice, entryPrice, oraclePrice) + matchedBalance = desiredBalance - balanceTotal - isolatedPnlImpact(marginMode, (desiredBalance - balanceTotal) * tradeLeverage / entryPrice, entryPrice, oraclePrice, isReduceOnly) matchedUsdcSize = matchedBalance * tradeLeverage matchedSize = matchedUsdcSize / entryPrice matchedSize = @@ -664,7 +669,7 @@ internal class TradeInputCalculator( stepSize, ) matchedUsdcSize = matchedSize * entryPrice - matchedBalance = matchedUsdcSize / tradeLeverage + isolatedPnlImpact(marginMode, matchedSize, entryPrice, oraclePrice) + matchedBalance = matchedUsdcSize / tradeLeverage + isolatedPnlImpact(marginMode, matchedSize, entryPrice, oraclePrice, isReduceOnly) } sizeTotal += matchedSize usdcSizeTotal += matchedUsdcSize 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 e49fc214a..d1b6d3d1c 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 @@ -181,6 +181,7 @@ internal class TradeInputMarketOrderCalculator() { 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, @@ -193,6 +194,7 @@ internal class TradeInputMarketOrderCalculator() { orderbook = orderbook, stepSize = stepSize, oraclePrice = oraclePrice, + isReduceOnly = isReduceOnly, ) } @@ -213,11 +215,12 @@ internal class TradeInputMarketOrderCalculator() { } } - private fun isolatedPnlImpact(marginMode: MarginMode, size: Double, entryPrice: Double, oraclePrice: Double?): Double { + 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 -> (entryPrice - (oraclePrice ?: entryPrice)).abs() * size + MarginMode.Isolated -> if (isReduceOnly) Numeric.double.ZERO else (entryPrice - (oraclePrice ?: entryPrice)).abs() * size } } @@ -232,6 +235,7 @@ internal class TradeInputMarketOrderCalculator() { orderbook: List?, stepSize: Double, oraclePrice: Double?, + isReduceOnly: Boolean, ): TradeInputMarketOrder? { 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 @@ -272,14 +276,14 @@ internal class TradeInputMarketOrderCalculator() { if (entryPrice > Numeric.double.ZERO) { val entryUsdcSize = entrySize * entryPrice val entryBalanceSize = entryUsdcSize / tradeLeverage - filled = (balanceTotal + entryBalanceSize + isolatedPnlImpact(marginMode = marginMode, size = entrySize, entryPrice = entryPrice, oraclePrice = oraclePrice)) >= 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 + isolatedPnlImpact(marginMode = marginMode, size = matchedSize, entryPrice = entryPrice, oraclePrice = oraclePrice) + var matchedBalance = matchedUsdcSize / tradeLeverage + isolatedPnlImpact(marginMode = marginMode, size = matchedSize, entryPrice = entryPrice, oraclePrice = oraclePrice, isReduceOnly = isReduceOnly) if (filled) { - matchedBalance = desiredBalance - balanceTotal + isolatedPnlImpact(marginMode = marginMode, size = (desiredBalance - balanceTotal) * tradeLeverage / entryPrice, entryPrice = entryPrice, oraclePrice = oraclePrice) + matchedBalance = desiredBalance - balanceTotal + isolatedPnlImpact(marginMode = marginMode, size = (desiredBalance - balanceTotal) * tradeLeverage / entryPrice, entryPrice = entryPrice, oraclePrice = oraclePrice, isReduceOnly = isReduceOnly) matchedUsdcSize = matchedBalance * tradeLeverage matchedSize = matchedUsdcSize / entryPrice matchedSize = @@ -288,7 +292,7 @@ internal class TradeInputMarketOrderCalculator() { stepSize, ) matchedUsdcSize = matchedSize * entryPrice - matchedBalance = matchedUsdcSize / tradeLeverage + isolatedPnlImpact(marginMode = marginMode, size = matchedSize, entryPrice = entryPrice, oraclePrice = oraclePrice) + matchedBalance = matchedUsdcSize / tradeLeverage + isolatedPnlImpact(marginMode = marginMode, size = matchedSize, entryPrice = entryPrice, oraclePrice = oraclePrice, isReduceOnly = isReduceOnly) } sizeTotal += matchedSize usdcSizeTotal += matchedUsdcSize From c6fd6c16f558fcd6d46b0563884dfdf4efaec513 Mon Sep 17 00:00:00 2001 From: mulan xia Date: Fri, 13 Sep 2024 17:05:04 -0400 Subject: [PATCH 8/9] bump v --- build.gradle.kts | 2 +- v4_abacus.podspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index e95a6ab7a..2591c8b9d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -52,7 +52,7 @@ allprojects { } group = "exchange.dydx.abacus" -version = "1.11.2" +version = "1.11.3" repositories { google() diff --git a/v4_abacus.podspec b/v4_abacus.podspec index decd99549..58dc4c9ac 100644 --- a/v4_abacus.podspec +++ b/v4_abacus.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = 'v4_abacus' - spec.version = '1.11.2' + spec.version = '1.11.3' spec.homepage = 'https://github.com/dydxprotocol/v4-abacus' spec.source = { :http=> ''} spec.authors = '' From a6a65b1c5bdebb2aa41d9ba5f190d579d24c5d8d Mon Sep 17 00:00:00 2001 From: mulan xia Date: Mon, 16 Sep 2024 10:32:03 -0400 Subject: [PATCH 9/9] review comments --- .../calculator/TradeInputCalculator.kt | 8 ++++++-- .../V2/TradeInput/TradeInputMarketOrderCalculator.kt | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt index 218f192c0..59e9ee971 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt @@ -476,7 +476,7 @@ internal class TradeInputCalculator( isBuying: Boolean, input: String, ): Map? { - val marketId = parser.asString(trade["marketId"]) + val marketId = parser.asString(market?.get("id")) val tradeSize = parser.asNativeMap(trade["size"]) if (tradeSize != null && marketId != null) { @@ -620,6 +620,10 @@ internal class TradeInputCalculator( 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 @@ -1093,7 +1097,7 @@ internal class TradeInputCalculator( tradeLeverage: Double, isTradeSameSide: Boolean, ): Double { - if (freeCollateral <= Numeric.double.ZERO) { + if (freeCollateral <= Numeric.double.ZERO || tradeLeverage <= Numeric.double.ZERO) { return Numeric.double.ZERO } return if (isTradeSameSide) { 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 d1b6d3d1c..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 @@ -640,7 +640,7 @@ internal class TradeInputMarketOrderCalculator() { tradeLeverage: Double, isTradeSameSide: Boolean, ): Double { - if (freeCollateral <= Numeric.double.ZERO) { + if (freeCollateral <= Numeric.double.ZERO || tradeLeverage <= Numeric.double.ZERO) { return Numeric.double.ZERO } return if (isTradeSameSide) {