From c1111d4fe446bddd2cb433619b631234760fca84 Mon Sep 17 00:00:00 2001 From: aleka Date: Thu, 22 Aug 2024 22:04:29 +0200 Subject: [PATCH] feat: limit close orders with default mid-market price as limit price (#589) Co-authored-by: mobile-build-bot-git --- build.gradle.kts | 2 +- .../calculator/TradeInputCalculator.kt | 7 +- .../state/manager/Environment.kt | 1 + .../TradingStateMachine+ClosePositionInput.kt | 48 ++++++- .../SubaccountTransactionPayloadProvider.kt | 12 +- .../exchange.dydx.abacus/utils/Constants.kt | 3 + .../payload/v4/V4ClosePositionTests.kt | 119 ++++++++++++++++++ v4_abacus.podspec | 2 +- 8 files changed, 185 insertions(+), 9 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index be181605c..46f1a817c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -52,7 +52,7 @@ allprojects { } group = "exchange.dydx.abacus" -version = "1.8.100" +version = "1.8.101" repositories { google() diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt index 86f769989..39986e5a5 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt @@ -107,6 +107,7 @@ internal class TradeInputCalculator( calculateNonMarketTrade( trade, market, + subaccount, type, isBuying, input, @@ -147,19 +148,21 @@ internal class TradeInputCalculator( private fun calculateNonMarketTrade( trade: Map, market: Map?, + subaccount: Map?, type: String, isBuying: Boolean, input: String, ): Map { val modifiedTrade = trade.mutable() - val tradeSize = parser.asNativeMap(trade["size"]) + val modified = calculateSize(trade, subaccount, market) + val tradeSize = parser.asNativeMap(modified["size"])?.mutable() val tradePrices = parser.asNativeMap(trade["price"]) val stepSize = parser.asDouble(parser.value(market, "configs.stepSize") ?: 0.001)!! if (tradeSize != null) { val modifiedTradeSize = tradeSize.mutable() when (input) { - "size.size" -> { + "size.size", "size.percent" -> { val price = nonMarketOrderPrice(tradePrices, market, type, isBuying) val size = parser.asDouble(tradeSize.get("size")) val usdcSize = diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/Environment.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/Environment.kt index 5bb8032e9..9e48c8f77 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/Environment.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/Environment.kt @@ -547,6 +547,7 @@ data object StatsigConfig { var useSkip: Boolean = false var ff_enable_evm_swaps: Boolean = false var dc_max_safe_bridge_fees: Float = Float.POSITIVE_INFINITY + var ff_enable_limit_close: Boolean = false } @JsExport 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 d1d185c68..bd99514ac 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 @@ -9,6 +9,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.state.manager.StatsigConfig import exchange.dydx.abacus.utils.Numeric import exchange.dydx.abacus.utils.mutable import exchange.dydx.abacus.utils.mutableMapOf @@ -22,7 +23,10 @@ import kotlinx.serialization.Serializable enum class ClosePositionInputField(val rawValue: String) { market("market"), size("size.size"), - percent("size.percent"); + percent("size.percent"), + + useLimit("useLimit"), + limitPrice("price.limitPrice"); companion object { operator fun invoke(rawValue: String?) = @@ -105,6 +109,35 @@ fun TradingStateMachine.closePosition( subaccountNumberChanges, ) } + ClosePositionInputField.useLimit.rawValue -> { + val useLimitClose = (parser.asBool(data) ?: false) && StatsigConfig.ff_enable_limit_close + trade.safeSet(typeText, useLimitClose) + + if (useLimitClose) { + trade["type"] = "LIMIT" + trade["timeInForce"] = "GTT" + parser.asString(trade["marketId"])?.let { + trade.safeSet("price.limitPrice", getMidMarketPrice(it)) + } + } else { + trade["type"] = "MARKET" + trade["timeInForce"] = "IOC" + } + + changes = StateChanges( + iListOf(Changes.subaccount, Changes.input), + null, + subaccountNumberChanges, + ) + } + ClosePositionInputField.limitPrice.rawValue -> { + trade.safeSet(typeText, parser.asDouble(data)) + changes = StateChanges( + iListOf(Changes.subaccount, Changes.input), + null, + subaccountNumberChanges, + ) + } else -> {} } if (sizeChanged) { @@ -178,3 +211,16 @@ private fun TradingStateMachine.initiateClosePosition( return parser.asMap(modified["trade"])?.mutable() ?: trade } + +private fun TradingStateMachine.getMidMarketPrice( + marketId: String +): Double? { + val markets = parser.asNativeMap(marketsSummary?.get("markets")) + return parser.asNativeMap(parser.asNativeMap(markets?.get(marketId))?.get("orderbook_consolidated"))?.let { orderbook -> + parser.asDouble(parser.value(orderbook, "asks.0.price"))?.let { firstAskPrice -> + parser.asDouble(parser.value(orderbook, "bids.0.price"))?.let { firstBidPrice -> + (firstAskPrice + firstBidPrice) / 2.0 + } + } + } +} diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/SubaccountTransactionPayloadProvider.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/SubaccountTransactionPayloadProvider.kt index a9699734c..681354612 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/SubaccountTransactionPayloadProvider.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/SubaccountTransactionPayloadProvider.kt @@ -18,6 +18,7 @@ import exchange.dydx.abacus.state.manager.HumanReadableTriggerOrdersPayload import exchange.dydx.abacus.state.manager.HumanReadableWithdrawPayload import exchange.dydx.abacus.state.manager.PlaceOrderMarketInfo import exchange.dydx.abacus.state.model.TradingStateMachine +import exchange.dydx.abacus.utils.LIMIT_CLOSE_ORDER_DEFAULT_DURATION_DAYS import exchange.dydx.abacus.utils.MAX_SUBACCOUNT_NUMBER import exchange.dydx.abacus.utils.NUM_PARENT_SUBACCOUNTS import exchange.dydx.abacus.utils.SHORT_TERM_ORDER_DURATION @@ -248,19 +249,22 @@ internal class SubaccountTransactionPayloadProvider( @Throws(Exception::class) override fun closePositionPayload(currentHeight: Int?): HumanReadablePlaceOrderPayload { val closePosition = stateMachine.state?.input?.closePosition + val isLimitClose = closePosition?.type == OrderType.Limit val marketId = closePosition?.marketId ?: throw Exception("marketId is null") val summary = closePosition.summary ?: throw Exception("summary is null") val clientId = Random.nextInt(0, Int.MAX_VALUE) + val type = closePosition.type?.rawValue ?: "MARKET" val side = closePosition.side?.rawValue ?: throw Exception("side is null") val price = summary.payloadPrice ?: throw Exception("price is null") val size = summary.size ?: throw Exception("size is null") val sizeInput = null - val timeInForce = "IOC" + val timeInForce = if (isLimitClose) "GTT" else "IOC" val execution = "DEFAULT" val reduceOnly = true val postOnly = false - val goodTilTimeInSeconds = null - val goodTilBlock = currentHeight?.plus(SHORT_TERM_ORDER_DURATION) + val limitCloseDuration = TradeInputGoodUntil(LIMIT_CLOSE_ORDER_DEFAULT_DURATION_DAYS, "D").timeInterval ?: throw Exception("invalid duration") + val goodTilTimeInSeconds = if (isLimitClose) (limitCloseDuration / 1.seconds).toInt() else null + val goodTilBlock = if (isLimitClose) null else currentHeight?.plus(SHORT_TERM_ORDER_DURATION) val marketInfo = marketInfo(marketId) val subaccountNumberForPosition = helper.parser.asInt(helper.parser.value(stateMachine.data, "wallet.account.groupedSubaccounts.$subaccountNumber.openPositions.$marketId.childSubaccountNumber")) ?: subaccountNumber @@ -268,7 +272,7 @@ internal class SubaccountTransactionPayloadProvider( subaccountNumber = subaccountNumberForPosition, marketId = marketId, clientId = clientId, - type = "MARKET", + type = type, side = side, price = price, triggerPrice = null, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/utils/Constants.kt b/src/commonMain/kotlin/exchange.dydx.abacus/utils/Constants.kt index 54f672614..88132dc23 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/utils/Constants.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/utils/Constants.kt @@ -31,3 +31,6 @@ internal const val MIN_USDC_AMOUNT_FOR_AUTO_SWEEP = 50000 // Gas Constants based on historical Squid responses internal const val DEFAULT_GAS_LIMIT = 1500000 internal const val DEFAULT_GAS_PRICE = 1520000000 + +// Limit Close GTT duration in Days +internal const val LIMIT_CLOSE_ORDER_DEFAULT_DURATION_DAYS = 28.0 diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4ClosePositionTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4ClosePositionTests.kt index 2308a45ea..262e586b8 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4ClosePositionTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4ClosePositionTests.kt @@ -1,5 +1,6 @@ package exchange.dydx.abacus.payload.v4 +import exchange.dydx.abacus.state.manager.StatsigConfig import exchange.dydx.abacus.state.model.ClosePositionInputField import exchange.dydx.abacus.state.model.closePosition import exchange.dydx.abacus.tests.extensions.log @@ -17,6 +18,8 @@ class V4ClosePositionTests : V4BaseTests() { testCloseShortPositionInput() time = perp.log("Close Position", time) + + testLimitClosePositionInput() } override fun setup() { @@ -285,4 +288,120 @@ class V4ClosePositionTests : V4BaseTests() { """.trimIndent(), ) } + + private fun testLimitClosePositionInput() { + StatsigConfig.ff_enable_limit_close = true + test( + { + perp.socket(mock.socketUrl, mock.accountsChannel.v4_subscribed, 0, null) + }, + """ + { + } + """.trimIndent(), + ) + /* + Initial setup + */ + test( + { + perp.closePosition("ETH-USD", ClosePositionInputField.market, 0) + }, + """ + { + "input": { + "current": "closePosition", + "closePosition": { + "type": "MARKET", + "side": "BUY", + "size": { + "percent": 1, + "input": "size.percent", + "size": 106.179 + }, + "reduceOnly": true + } + } + } + """.trimIndent(), + ) + + test( + { + perp.closePosition("true", ClosePositionInputField.useLimit, 0) + }, + """ + { + "input": { + "current": "closePosition", + "closePosition": { + "type": "LIMIT", + "side": "BUY", + "size": { + "percent": 1, + "input": "size.percent", + "size": 106.179 + }, + "price": { + "limitPrice": 2000 + }, + "reduceOnly": true + } + } + } + """.trimIndent(), + ) + + test( + { + perp.closePosition("2500", ClosePositionInputField.limitPrice, 0) + }, + """ + { + "input": { + "current": "closePosition", + "closePosition": { + "type": "LIMIT", + "side": "BUY", + "size": { + "percent": 1, + "input": "size.percent", + "size": 106.179 + }, + "price": { + "limitPrice": 2500 + }, + "reduceOnly": true + } + } + } + """.trimIndent(), + ) + + test( + { + perp.closePosition("false", ClosePositionInputField.useLimit, 0) + }, + """ + { + "input": { + "current": "closePosition", + "closePosition": { + "type": "MARKET", + "side": "BUY", + "size": { + "percent": 1, + "input": "size.percent", + "size": 106.179 + }, + "price": { + "limitPrice": 2500 + }, + "reduceOnly": true + } + } + } + """.trimIndent(), + ) + } } diff --git a/v4_abacus.podspec b/v4_abacus.podspec index f08e31e28..68845b8c9 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.8.100' + spec.version = '1.8.101' spec.homepage = 'https://github.com/dydxprotocol/v4-abacus' spec.source = { :http=> ''} spec.authors = ''