Skip to content

Commit

Permalink
feat: limit close orders with default mid-market price as limit price (
Browse files Browse the repository at this point in the history
…#589)

Co-authored-by: mobile-build-bot-git <[email protected]>
  • Loading branch information
aforaleka and mobile-build-bot authored Aug 22, 2024
1 parent 0a93ee0 commit c1111d4
Show file tree
Hide file tree
Showing 8 changed files with 185 additions and 9 deletions.
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ allprojects {
}

group = "exchange.dydx.abacus"
version = "1.8.100"
version = "1.8.101"

repositories {
google()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ internal class TradeInputCalculator(
calculateNonMarketTrade(
trade,
market,
subaccount,
type,
isBuying,
input,
Expand Down Expand Up @@ -147,19 +148,21 @@ internal class TradeInputCalculator(
private fun calculateNonMarketTrade(
trade: Map<String, Any>,
market: Map<String, Any>?,
subaccount: Map<String, Any>?,
type: String,
isBuying: Boolean,
input: String,
): Map<String, Any> {
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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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?) =
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -248,27 +249,30 @@ 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

return HumanReadablePlaceOrderPayload(
subaccountNumber = subaccountNumberForPosition,
marketId = marketId,
clientId = clientId,
type = "MARKET",
type = type,
side = side,
price = price,
triggerPrice = null,
Expand Down
3 changes: 3 additions & 0 deletions src/commonMain/kotlin/exchange.dydx.abacus/utils/Constants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -17,6 +18,8 @@ class V4ClosePositionTests : V4BaseTests() {

testCloseShortPositionInput()
time = perp.log("Close Position", time)

testLimitClosePositionInput()
}

override fun setup() {
Expand Down Expand Up @@ -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(),
)
}
}
2 changes: 1 addition & 1 deletion v4_abacus.podspec
Original file line number Diff line number Diff line change
@@ -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 = ''
Expand Down

0 comments on commit c1111d4

Please sign in to comment.