diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginCalculator.kt index c12df5874..941d40bd8 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginCalculator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginCalculator.kt @@ -676,6 +676,19 @@ internal object MarginCalculator { } ?: return null } + private fun getMaxMarketLeverageDeprecated( + effectiveImf: Double, + imf: Double, + ): Double { + return if (effectiveImf > Numeric.double.ZERO) { + Numeric.double.ONE / effectiveImf + } else if (imf > Numeric.double.ZERO) { + Numeric.double.ONE / imf + } else { + Numeric.double.ONE + } + } + /** * @description Calculate the amount of collateral to transfer for an isolated margin trade. * Max leverage is capped at 98% of the the market's max leverage and takes the oraclePrice into account in order to pass collateral checks. @@ -685,12 +698,11 @@ internal object MarginCalculator { market: InternalMarketState?, subaccount: InternalSubaccountState? ): Double? { - val targetLeverage = trade.targetLeverage ?: 1.0 val side = trade.side ?: return null val oraclePrice = market?.perpetualMarket?.oraclePrice ?: return null val price = trade.summary?.price ?: return null - val initialMarginFraction = market.perpetualMarket?.configs?.initialMarginFraction ?: 0.0 - val effectiveImf = market.perpetualMarket?.configs?.effectiveInitialMarginFraction ?: 0.0 + val maxMarketLeverage = market.perpetualMarket?.configs?.maxMarketLeverage ?: return null + val targetLeverage = trade.targetLeverage ?: maxMarketLeverage val positionSizeDifference = getPositionSizeDifference(subaccount, trade) ?: return null return calculateIsolatedMarginTransferAmountFromValues( @@ -698,8 +710,7 @@ internal object MarginCalculator { side = side.rawValue, oraclePrice = oraclePrice, price = price, - initialMarginFraction = initialMarginFraction, - effectiveImf = effectiveImf, + maxMarketLeverage = maxMarketLeverage, positionSizeDifference = positionSizeDifference, ) } @@ -714,12 +725,14 @@ internal object MarginCalculator { market: Map?, subaccount: Map? ): Double? { - val targetLeverage = parser.asDouble(trade["targetLeverage"]) ?: 1.0 val side = parser.asString(parser.value(trade, "side")) ?: return null val oraclePrice = parser.asDouble(parser.value(market, "oraclePrice")) ?: return null val price = parser.asDouble(parser.value(trade, "summary.price")) ?: return null - val initialMarginFraction = parser.asDouble(parser.value(market, "configs.initialMarginFraction")) ?: 0.0 - val effectiveImf = parser.asDouble(parser.value(market, "configs.effectiveInitialMarginFraction")) ?: 0.0 + val initialMarginFraction = parser.asDouble(parser.value(market, "configs.initialMarginFraction")) ?: Numeric.double.ZERO + val effectiveImf = parser.asDouble(parser.value(market, "configs.effectiveInitialMarginFraction")) ?: Numeric.double.ZERO + val maxMarketLeverage = getMaxMarketLeverageDeprecated(effectiveImf = effectiveImf, imf = initialMarginFraction) + + val targetLeverage = parser.asDouble(trade["targetLeverage"]) ?: maxMarketLeverage val positionSizeDifference = getPositionSizeDifferenceDeprecated(parser, subaccount, trade) ?: return null return calculateIsolatedMarginTransferAmountFromValues( @@ -727,8 +740,7 @@ internal object MarginCalculator { side = side, oraclePrice = oraclePrice, price = price, - initialMarginFraction = initialMarginFraction, - effectiveImf = effectiveImf, + maxMarketLeverage = maxMarketLeverage, positionSizeDifference = positionSizeDifference, ) } @@ -742,8 +754,7 @@ internal object MarginCalculator { val side = trade.side?.rawValue ?: return null val oraclePrice = market.oraclePrice ?: return null val price = trade.summary?.price ?: return null - val initialMarginFraction = market.configs?.initialMarginFraction ?: 0.0 - val effectiveImf = market.configs?.effectiveInitialMarginFraction ?: 0.0 + val maxMarketLeverage = market.configs?.maxMarketLeverage ?: return null val positionSizeDifference = getPositionSizeDifference(subaccount, trade) ?: return null return calculateIsolatedMarginTransferAmountFromValues( @@ -751,8 +762,7 @@ internal object MarginCalculator { side, oraclePrice, price, - initialMarginFraction, - effectiveImf, + maxMarketLeverage, positionSizeDifference, ) } @@ -762,27 +772,14 @@ internal object MarginCalculator { side: String, oraclePrice: Double, price: Double, - initialMarginFraction: Double, - effectiveImf: Double, + maxMarketLeverage: Double?, positionSizeDifference: Double, ): Double? { - val maxLeverageForMarket = if (effectiveImf != 0.0) { - 1.0 / effectiveImf - } else if (initialMarginFraction != 0.0) { - 1.0 / initialMarginFraction - } else { - null - } - + val maxLeverageForMarket = maxMarketLeverage ?: Numeric.double.ONE // Cap targetLeverage to 98% of max leverage - val adjustedTargetLeverage = if (maxLeverageForMarket != null) { - val cappedLeverage = maxLeverageForMarket * MAX_LEVERAGE_BUFFER_PERCENT - min(targetLeverage, cappedLeverage) - } else { - null - } + val adjustedTargetLeverage = min(targetLeverage, maxLeverageForMarket * MAX_LEVERAGE_BUFFER_PERCENT) - return if (adjustedTargetLeverage == 0.0 || adjustedTargetLeverage == null) { + return if (adjustedTargetLeverage == 0.0) { null } else { getTransferAmountFromTargetLeverage( diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginModeCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginModeCalculator.kt index 1d65090a4..43921c579 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginModeCalculator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginModeCalculator.kt @@ -1,5 +1,6 @@ package exchange.dydx.abacus.calculator +import exchange.dydx.abacus.utils.Numeric import exchange.dydx.abacus.utils.Parser import exchange.dydx.abacus.utils.mutable @@ -15,6 +16,18 @@ object MarginModeCalculator { ): MutableMap? { val modified = tradeInput?.mutable() ?: return null val marketId = parser.asString(tradeInput["marketId"]) + val market = parser.asMap(markets?.get(marketId)) + + val imf = parser.asDouble(parser.value(market, "configs.initialMarginFraction")) ?: Numeric.double.ZERO + val effectiveImf = parser.asDouble(parser.value(market, "configs.effectiveInitialMarginFraction")) ?: Numeric.double.ZERO + val maxMarketLeverage = if (effectiveImf > Numeric.double.ZERO) { + Numeric.double.ONE / effectiveImf + } else if (imf > Numeric.double.ZERO) { + Numeric.double.ONE / imf + } else { + Numeric.double.ONE + } + val existingMarginMode = MarginCalculator.findExistingMarginModeDeprecated( parser, @@ -36,18 +49,18 @@ object MarginModeCalculator { subaccountNumber, ) val existingPositionLeverage = parser.asDouble(parser.value(existingPosition, "leverage.current")) - modified["targetLeverage"] = existingPositionLeverage ?: 1.0 + modified["targetLeverage"] = existingPositionLeverage ?: maxMarketLeverage } } else { val marketMarginMode = MarginCalculator.findMarketMarginModeDeprecated( parser, - parser.asMap(markets?.get(marketId)), + market, ) when (marketMarginMode) { "ISOLATED" -> { modified["marginMode"] = marketMarginMode if (parser.asDouble(tradeInput["targetLeverage"]) == null) { - modified["targetLeverage"] = 1.0 + modified["targetLeverage"] = maxMarketLeverage } } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt index 39986e5a5..06b26b166 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt @@ -78,7 +78,6 @@ internal class TradeInputCalculator( val subaccount = if (marginMode == MarginMode.Cross) { crossMarginSubaccount } else { - // TODO: incorrect for isolated trades; fix CT-1092 groupedSubaccount } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputMarginModeCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputMarginModeCalculator.kt index 4b0dc151a..9e5c48758 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputMarginModeCalculator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputMarginModeCalculator.kt @@ -6,6 +6,7 @@ import exchange.dydx.abacus.output.input.MarginMode import exchange.dydx.abacus.state.internalstate.InternalAccountState import exchange.dydx.abacus.state.internalstate.InternalMarketState import exchange.dydx.abacus.state.internalstate.InternalTradeInputState +import exchange.dydx.abacus.utils.Numeric internal class TradeInputMarginModeCalculator { fun updateTradeInputMarginMode( @@ -19,6 +20,8 @@ internal class TradeInputMarginModeCalculator { marketId = tradeInput.marketId, subaccountNumber = subaccountNumber, ) + val market = markets?.get(tradeInput.marketId) + val maxMarketLeverage = market?.perpetualMarket?.configs?.maxMarketLeverage ?: Numeric.double.ONE // If there is an existing position or order, we have to use the same margin mode if (existingMarginMode != null) { @@ -30,16 +33,16 @@ internal class TradeInputMarginModeCalculator { subaccountNumber = subaccountNumber, ) val existingPositionLeverage = existingPosition?.calculated?.get(CalculationPeriod.current)?.leverage - tradeInput.targetLeverage = existingPositionLeverage ?: 1.0 + tradeInput.targetLeverage = if (existingPositionLeverage != null && existingPositionLeverage > Numeric.double.ZERO) existingPositionLeverage else maxMarketLeverage } } else { val marketMarginMode = MarginCalculator.findMarketMarginMode( - market = markets?.get(tradeInput.marketId)?.perpetualMarket, + market = market?.perpetualMarket, ) when (marketMarginMode) { MarginMode.Isolated -> { tradeInput.marginMode = marketMarginMode - tradeInput.targetLeverage = tradeInput.targetLeverage ?: 1.0 + tradeInput.targetLeverage = tradeInput.targetLeverage ?: maxMarketLeverage } MarginMode.Cross -> { diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/output/Market.kt b/src/commonMain/kotlin/exchange.dydx.abacus/output/Market.kt index 240bfb9f5..8229b0aed 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/output/Market.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/output/Market.kt @@ -11,6 +11,7 @@ import exchange.dydx.abacus.state.manager.OrderbookGrouping import exchange.dydx.abacus.utils.IList import exchange.dydx.abacus.utils.IMap import exchange.dydx.abacus.utils.Logger +import exchange.dydx.abacus.utils.Numeric import exchange.dydx.abacus.utils.ParsingHelper import exchange.dydx.abacus.utils.mutable import exchange.dydx.abacus.utils.typedSafeSet @@ -232,6 +233,19 @@ data class MarketConfigs( } } } + + internal val maxMarketLeverage: Double + get() { + val imf = initialMarginFraction ?: Numeric.double.ZERO + val effectiveImf = effectiveInitialMarginFraction ?: Numeric.double.ZERO + return if (effectiveImf > Numeric.double.ZERO) { + Numeric.double.ONE / effectiveImf + } else if (imf > Numeric.double.ZERO) { + Numeric.double.ONE / imf + } else { + Numeric.double.ONE + } + } } @JsExport diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/ClosePositionInputProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/ClosePositionInputProcessor.kt index 47b273b74..566298c3f 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/ClosePositionInputProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/ClosePositionInputProcessor.kt @@ -103,8 +103,11 @@ internal class ClosePositionInputProcessor( trade.timeInForce = "IOC" trade.reduceOnly = true + val market = marketSummaryState.markets.get(trade.marketId) + val maxMarketLeverage = market?.perpetualMarket?.configs?.maxMarketLeverage ?: Numeric.double.ONE + val currentPositionLeverage = position.calculated[CalculationPeriod.current]?.leverage?.abs() - trade.targetLeverage = if (currentPositionLeverage != null && currentPositionLeverage > 0) currentPositionLeverage else 1.0 + trade.targetLeverage = if (currentPositionLeverage != null && currentPositionLeverage > Numeric.double.ZERO) currentPositionLeverage else maxMarketLeverage // default full close trade.sizePercent = 1.0 diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputProcessor.kt index dcd891114..2db9de036 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputProcessor.kt @@ -28,6 +28,7 @@ import exchange.dydx.abacus.state.internalstate.InternalTradeInputState import exchange.dydx.abacus.state.internalstate.InternalWalletState import exchange.dydx.abacus.state.internalstate.safeCreate import exchange.dydx.abacus.state.model.TradeInputField +import exchange.dydx.abacus.utils.Numeric import kollections.iListOf internal interface TradeInputProcessorProtocol { @@ -101,6 +102,7 @@ internal class TradeInputProcessor( initiateMarginModeLeverage( trade = inputState.trade, marketState = market, + marketSummaryState = marketSummaryState, accountState = walletState.account, marketId = marketId, subaccountNumber = subaccountNumber, @@ -274,6 +276,7 @@ internal class TradeInputProcessor( private fun initiateMarginModeLeverage( trade: InternalTradeInputState, marketState: InternalMarketState?, + marketSummaryState: InternalMarketSummaryState, accountState: InternalAccountState, marketId: String, subaccountNumber: Int, @@ -289,18 +292,21 @@ internal class TradeInputProcessor( marketId = marketId, subaccountNumber = subaccountNumber, ) + val market = marketSummaryState.markets[marketId] + val maxMarketLeverage = market?.perpetualMarket?.configs?.maxMarketLeverage ?: Numeric.double.ONE + if (existingPosition != null) { trade.marginMode = if (subaccount?.equity != null) MarginMode.Isolated else MarginMode.Cross val currentPositionLeverage = existingPosition.calculated[CalculationPeriod.current]?.leverage?.abs() val positionLeverage = - if (currentPositionLeverage != null && currentPositionLeverage > 0) currentPositionLeverage else 1.0 + if (currentPositionLeverage != null && currentPositionLeverage > Numeric.double.ZERO) currentPositionLeverage else Numeric.double.ONE trade.targetLeverage = positionLeverage } else if (existingOrder != null) { trade.marginMode = if (existingOrder.subaccountNumber == subaccountNumber) MarginMode.Cross else MarginMode.Isolated - trade.targetLeverage = 1.0 + trade.targetLeverage = maxMarketLeverage } else { val marketType = marketState?.perpetualMarket?.configs?.perpetualMarketType trade.marginMode = when (marketType) { @@ -308,7 +314,7 @@ internal class TradeInputProcessor( PerpetualMarketType.ISOLATED -> MarginMode.Isolated else -> null } - trade.targetLeverage = 1.0 + trade.targetLeverage = maxMarketLeverage } } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+ClosePositionInput.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+ClosePositionInput.kt index 7bab0089b..1b7cd31c6 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+ClosePositionInput.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+ClosePositionInput.kt @@ -104,8 +104,18 @@ fun TradingStateMachine.closePosition( trade["timeInForce"] = "IOC" trade["reduceOnly"] = true + val market = parser.asNativeMap(parser.value(marketsSummary, "markets.${trade["marketId"]}")) + val imf = parser.asDouble(parser.value(market, "configs.initialMarginFraction")) ?: Numeric.double.ZERO + val effectiveImf = parser.asDouble(parser.value(market, "configs.effectiveInitialMarginFraction")) ?: Numeric.double.ZERO + val maxMarketLeverage = if (effectiveImf > Numeric.double.ZERO) { + Numeric.double.ONE / effectiveImf + } else if (imf > Numeric.double.ZERO) { + Numeric.double.ONE / imf + } else { + Numeric.double.ONE + } val currentPositionLeverage = parser.asDouble(parser.value(position, "leverage.current"))?.abs() - trade["targetLeverage"] = if (currentPositionLeverage != null && currentPositionLeverage > 0) currentPositionLeverage else 1.0 + trade["targetLeverage"] = if (currentPositionLeverage != null && currentPositionLeverage > 0) currentPositionLeverage else maxMarketLeverage // default full close trade.safeSet("size.percent", 1.0) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt index fba530bdb..d60d03b82 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt @@ -11,6 +11,7 @@ import exchange.dydx.abacus.responses.StateResponse import exchange.dydx.abacus.responses.cannotModify import exchange.dydx.abacus.state.changes.Changes import exchange.dydx.abacus.state.changes.StateChanges +import exchange.dydx.abacus.utils.Numeric import exchange.dydx.abacus.utils.mutable import exchange.dydx.abacus.utils.mutableMapOf import exchange.dydx.abacus.utils.safeSet @@ -150,6 +151,16 @@ internal fun TradingStateMachine.tradeInMarket( marketId, subaccountNumber, ) + val market = parser.asNativeMap(parser.value(marketsSummary, "markets.$marketId")) + val imf = parser.asDouble(parser.value(market, "configs.initialMarginFraction")) ?: Numeric.double.ZERO + val effectiveImf = parser.asDouble(parser.value(market, "configs.effectiveInitialMarginFraction")) ?: Numeric.double.ZERO + val maxMarketLeverage = if (effectiveImf > Numeric.double.ZERO) { + Numeric.double.ONE / effectiveImf + } else if (imf > Numeric.double.ZERO) { + Numeric.double.ONE / imf + } else { + Numeric.double.ONE + } if (existingPosition != null) { it.safeSet("marginMode", if (existingPosition["equity"] != null) MarginMode.Isolated.rawValue else MarginMode.Cross.rawValue) val currentPositionLeverage = parser.asDouble(parser.value(existingPosition, "leverage.current"))?.abs() @@ -158,11 +169,11 @@ internal fun TradingStateMachine.tradeInMarket( } else if (existingOrder != null) { val orderMarginMode = if ((parser.asInt(parser.value(existingOrder, "subaccountNumber")) ?: subaccountNumber) == subaccountNumber) MarginMode.Cross.rawValue else MarginMode.Isolated.rawValue it.safeSet("marginMode", orderMarginMode) - it.safeSet("targetLeverage", 1.0) + it.safeSet("targetLeverage", maxMarketLeverage) } else { val marketType = parser.asString(parser.value(marketsSummary, "markets.$marketId.configs.perpetualMarketType")) it.safeSet("marginMode", MarginMode.invoke(marketType)?.rawValue) - it.safeSet("targetLeverage", 1.0) + it.safeSet("targetLeverage", maxMarketLeverage) } } diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/payload/IsolatedMarginModeTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/payload/IsolatedMarginModeTests.kt index 93cfb3d01..86d8d8356 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/payload/IsolatedMarginModeTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/payload/IsolatedMarginModeTests.kt @@ -190,6 +190,7 @@ class IsolatedMarginModeTests : V4BaseTests(true) { @Test fun testMarginMode() { + testDefaultTargetLeverage() testMarginModeOnMarketChange() testMarginAmountForSubaccountTransfer() } @@ -200,6 +201,29 @@ class IsolatedMarginModeTests : V4BaseTests(true) { testMarginAmountForSubaccountTransferWithExistingPositionAndOpenOrders() } + private fun testDefaultTargetLeverage() { + test( + { + perp.tradeInMarket("NEAR-USD", 0) + }, + """ + { + "input": { + "current": "trade", + "trade": { + "marketId": "NEAR-USD", + "marginMode": "CROSS", + "targetLeverage": 10.0, + "options": { + "needsMarginMode": true + } + } + } + } + """.trimIndent(), + ) + } + // MarginMode should automatically to match the current market based on a variety of factors private fun testMarginModeOnMarketChange() { testParentSubaccountSubscribedWithPendingPositions() diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4TradeInputTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4TradeInputTests.kt index 5360a8545..795f85666 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4TradeInputTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4TradeInputTests.kt @@ -584,7 +584,7 @@ open class V4TradeInputTests : V4BaseTests() { }, null) test({ - perp.trade("0.01", TradeInputField.size, 0) + perp.trade("380", TradeInputField.usdcSize, 0) }, null) if (perp.staticTyping) { @@ -620,7 +620,7 @@ open class V4TradeInputTests : V4BaseTests() { "0": { "freeCollateral": { "current": 100000.0, - "postOrder": 99985.0 + "postOrder": 99981.0 } } } @@ -663,7 +663,7 @@ open class V4TradeInputTests : V4BaseTests() { } if (perp.staticTyping) { - perp.trade("0.1", TradeInputField.size, 0) + perp.trade("400", TradeInputField.usdcSize, 0) val subaccount = perp.internalState.wallet.account.subaccounts[0]!! assertEquals(100000.0, subaccount.calculated[CalculationPeriod.current]?.equity) @@ -671,7 +671,7 @@ open class V4TradeInputTests : V4BaseTests() { } else { test( { - perp.trade("0.1", TradeInputField.size, 0) + perp.trade("400", TradeInputField.usdcSize, 0) }, """ { @@ -681,7 +681,7 @@ open class V4TradeInputTests : V4BaseTests() { "0": { "equity": { "current": 100000.0, - "postOrder": 99850.0 + "postOrder": 99980.0 } } }