diff --git a/build.gradle.kts b/build.gradle.kts index 321de0235..df84668cc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -49,7 +49,7 @@ allprojects { } group = "exchange.dydx.abacus" -version = "1.6.47" +version = "1.6.48" repositories { google() diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/TriggerOrdersInputValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/TriggerOrdersInputValidator.kt index f1b9efa48..32f7e2b17 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/TriggerOrdersInputValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/TriggerOrdersInputValidator.kt @@ -1,5 +1,9 @@ package exchange.dydx.abacus.validator import abs +import exchange.dydx.abacus.output.input.OrderSide +import exchange.dydx.abacus.output.input.OrderStatus +import exchange.dydx.abacus.output.input.OrderTimeInForce +import exchange.dydx.abacus.output.input.OrderType import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.app.helper.Formatter @@ -7,13 +11,8 @@ import exchange.dydx.abacus.state.manager.BlockAndTime import exchange.dydx.abacus.state.manager.V4Environment import exchange.dydx.abacus.state.model.TriggerOrdersInputField import exchange.dydx.abacus.utils.Rounder - -internal data class EquityTier( - val requiredTotalNetCollateralUSD: Double, - val maxOrders: Int, -) { - var nextLevelRequiredTotalNetCollateralUSD: Double? = null -} +import exchange.dydx.abacus.validator.trade.EquityTier +import exchange.dydx.abacus.validator.trade.SubaccountLimitConstants.MAX_NUM_OPEN_UNTRIGGERED_ORDERS enum class RelativeToPrice(val rawValue: String) { ABOVE("ABOVE"), @@ -32,9 +31,6 @@ internal class TriggerOrdersInputValidator( ) : BaseInputValidator(localizer, formatter, parser), ValidatorProtocol { - @Suppress("PropertyName") - private val MAX_NUM_OPEN_UNTRIGGERED_ORDERS: Int = 20 - override fun validate( wallet: Map?, user: Map?, @@ -51,6 +47,7 @@ internal class TriggerOrdersInputValidator( val marketId = parser.asString(transaction["marketId"]) ?: return null val market = parser.asNativeMap(markets?.get(marketId)) + val position = parser.asNativeMap(parser.value(subaccount, "openPositions.$marketId")) ?: return null val tickSize = parser.asString(parser.value(market, "configs.tickSize")) ?: "0.01" val oraclePrice = parser.asDouble( parser.value( @@ -72,6 +69,7 @@ internal class TriggerOrdersInputValidator( market, oraclePrice, tickSize, + position, ) } else { null @@ -87,6 +85,7 @@ internal class TriggerOrdersInputValidator( market, oraclePrice, tickSize, + position, ) } else { null @@ -130,6 +129,7 @@ internal class TriggerOrdersInputValidator( market: Map?, oraclePrice: Double, tickSize: String, + position: Map, ): MutableList? { val triggerErrors = mutableListOf() @@ -166,9 +166,89 @@ internal class TriggerOrdersInputValidator( */ triggerErrors.addAll(it) } + + validateTriggerToLiquidationPrice(triggerOrder, position, tickSize)?.let { + /* + SELL_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE + BUY_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE + */ + triggerErrors.addAll(it) + } + return if (triggerErrors.size > 0) triggerErrors else null } + private fun validateTriggerToLiquidationPrice( + triggerOrder: Map, + position: Map, + tickSize: String, + ): List? { + val liquidationPrice = parser.asDouble(parser.value(position, "liquidationPrice.current")) + val triggerPrice = parser.asDouble(parser.value(triggerOrder, "price.triggerPrice")) + val type = parser.asString(triggerOrder["type"])?.let { OrderType.invoke(it) } + val side = parser.asString(triggerOrder["side"])?.let { OrderSide.invoke(it) } + + if (side == null || liquidationPrice == null || triggerPrice == null) { + return null + } + + return when (requiredTriggerToLiquidationPrice(type, side)) { + RelativeToPrice.ABOVE -> { + if (triggerPrice <= liquidationPrice) { + liquidationPriceError( + "SELL_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE", + "ERRORS.TRIGGERS_FORM_TITLE.SELL_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE", + "ERRORS.TRIGGERS_FORM.SELL_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE_NO_LIMIT", + liquidationPrice, + tickSize, + ) + } else { + null + } + } + RelativeToPrice.BELOW -> { + if (triggerPrice >= liquidationPrice) { + liquidationPriceError( + "BUY_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE", + "ERRORS.TRIGGERS_FORM_TITLE.BUY_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE", + "ERRORS.TRIGGERS_FORM.BUY_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE_NO_LIMIT", + liquidationPrice, + tickSize, + ) + } else { + null + } + } + else -> null + } + } + + private fun liquidationPriceError( + errorCode: String, + titleStringKey: String, + textStringKey: String, + liquidationPrice: Double?, + tickSize: String, + ): List? { + return listOf( + error( + "ERROR", + errorCode, + listOf(TriggerOrdersInputField.stopLossPrice.rawValue), + "APP.TRADE.MODIFY_TRIGGER_PRICE", + titleStringKey, + textStringKey, + mapOf( + "TRIGGER_PRICE_LIMIT" to mapOf( + "value" to liquidationPrice, + "format" to "price", + "tickSize" to tickSize, + ), + ), + ), + ) + } + private fun validateOrderCount( triggerOrders: Map, subaccount: Map?, @@ -286,42 +366,20 @@ internal class TriggerOrdersInputValidator( parser.asNativeMap(subaccount?.get("orders"))?.let { orders -> for ((_, item) in orders) { parser.asNativeMap(item)?.let { order -> - val status = parser.asString(order["status"]) - val orderType = parser.asString(order["type"]) - val timeInForce = parser.asString(order["timeInForce"]) - if (orderType != null && timeInForce != null) { - val isCurrentOrderStateful = isStatefulOrder(orderType, timeInForce) - // Short term with IOC or FOK should not be counted - val isShortTermAndRequiresImmediateExecution = - !isCurrentOrderStateful && (timeInForce == "IOC" || timeInForce == "FOK") - if (!isShortTermAndRequiresImmediateExecution && - (status == "OPEN" || status == "PENDING" || status == "UNTRIGGERED" || status == "PARTIALLY_FILLED") - ) { + val status = parser.asString(order["status"])?.let { OrderStatus.invoke(it) } + val orderType = parser.asString(order["type"])?.let { OrderType.invoke(it) } + val timeInForce = parser.asString(order["timeInForce"])?.let { OrderTimeInForce.invoke(it) } + if (orderType != null && timeInForce != null && status != null) { + if (isOrderIncludedInEquityTierLimit(orderType, timeInForce, status)) { count += 1 } } } } } - return count } - private fun isStatefulOrder(orderType: String, timeInForce: String): Boolean { - return when (orderType) { - "MARKET" -> false - - "LIMIT" -> { - when (parser.asString(timeInForce)) { - "GTT" -> true - else -> false - } - } - - else -> true - } - } - private fun validateRequiredInput( triggerOrder: Map, ): List? { @@ -342,11 +400,13 @@ internal class TriggerOrdersInputValidator( private fun validateCalculatedPricesPositive( triggerOrder: Map, ): List? { - val type = parser.asString(triggerOrder["type"]) + val type = parser.asString(triggerOrder["type"])?.let { + OrderType.invoke(it) + } val triggerPrice = parser.asDouble(parser.value(triggerOrder, "price.triggerPrice")) val limitPrice = parser.asDouble(parser.value(triggerOrder, "price.limitPrice")) val inputField = parser.asString(parser.value(triggerOrder, "price.input")) - val fields = if (type == "STOP_LIMIT" || type == "STOP_MARKET") { + val fields = if (type == OrderType.stopLimit || type == OrderType.stopMarket) { if (triggerPrice != null && triggerPrice <= 0) { listOfNotNull(inputField) } else if (limitPrice != null && limitPrice <= 0) { @@ -354,7 +414,7 @@ internal class TriggerOrdersInputValidator( } else { null } - } else if (type == "TAKE_PROFIT" || type == "TAKE_PROFIT_MARKET") { + } else if (type == OrderType.takeProfitLimit || type == OrderType.takeProfitMarket) { if (triggerPrice != null && triggerPrice <= 0) { listOfNotNull(inputField) } else if (limitPrice != null && limitPrice <= 0) { @@ -386,8 +446,16 @@ internal class TriggerOrdersInputValidator( oraclePrice: Double, tickSize: String, ): List? { - val type = parser.asString(triggerOrder["type"]) ?: return null - val side = parser.asString(triggerOrder["side"]) ?: return null + val type = parser.asString(triggerOrder["type"])?.let { + OrderType.invoke(it) + } + val side = parser.asString(triggerOrder["side"])?.let { + OrderSide.invoke(it) + } + + if (type == null || side == null) { + return null + } val triggerPrice = parser.asDouble(parser.value(triggerOrder, "price.triggerPrice")) ?: return null @@ -430,58 +498,83 @@ internal class TriggerOrdersInputValidator( private fun validateLimitPrice( triggerOrder: Map, ): List? { - val type = parser.asString(triggerOrder["type"]) - val fields = if (type == "STOP_LIMIT") { - listOf( - TriggerOrdersInputField.stopLossLimitPrice.rawValue, - ) - } else if (type == "TAKE_PROFIT") { - listOf( - TriggerOrdersInputField.takeProfitLimitPrice.rawValue, - ) - } else { - null + val type = parser.asString(triggerOrder["type"])?.let { OrderType.invoke(it) } + val side = parser.asString(triggerOrder["side"])?.let { OrderSide.invoke(it) } + + if (type == null || side == null) { + return null + } + + val fields = when (type) { + OrderType.stopLimit -> listOf(TriggerOrdersInputField.stopLossLimitPrice.rawValue) + OrderType.takeProfitLimit -> listOf(TriggerOrdersInputField.takeProfitLimitPrice.rawValue) + else -> null } + return when (type) { - "STOP_LIMIT", "TAKE_PROFIT" -> { - parser.asString(parser.value(triggerOrder, "side"))?.let { side -> - parser.asDouble(parser.value(triggerOrder, "price.limitPrice")) - ?.let { limitPrice -> - parser.asDouble(parser.value(triggerOrder, "price.triggerPrice")) - ?.let { triggerPrice -> - if (side == "BUY" && limitPrice < triggerPrice) { - return listOf( - error( - "ERROR", - "LIMIT_MUST_ABOVE_TRIGGER_PRICE", - fields, - "APP.TRADE.MODIFY_TRIGGER_PRICE", - if (type == "STOP_LIMIT") "ERRORS.TRIGGERS_FORM_TITLE.STOP_LOSS_LIMIT_MUST_ABOVE_TRIGGER_PRICE" else "ERRORS.TRIGGERS_FORM_TITLE.TAKE_PROFIT_LIMIT_MUST_ABOVE_TRIGGER_PRICE", - if (type == "STOP_LIMIT") "ERRORS.TRIGGERS_FORM.STOP_LOSS_LIMIT_MUST_ABOVE_TRIGGER_PRICE" else "ERRORS.TRIGGERS_FORM.TAKE_PROFIT_LIMIT_MUST_ABOVE_TRIGGER_PRICE", - ), - ) - } else if (side == "SELL" && limitPrice > triggerPrice) { - return listOf( - error( - "ERROR", - "LIMIT_MUST_BELOW_TRIGGER_PRICE", - fields, - "APP.TRADE.MODIFY_TRIGGER_PRICE", - if (type == "STOP_LIMIT") "ERRORS.TRIGGERS_FORM_TITLE.STOP_LOSS_LIMIT_MUST_BELOW_TRIGGER_PRICE" else "ERRORS.TRIGGERS_FORM_TITLE.TAKE_PROFIT_LIMIT_MUST_BELOW_TRIGGER_PRICE", - if (type == "STOP_LIMIT") "ERRORS.TRIGGERS_FORM.STOP_LOSS_LIMIT_MUST_BELOW_TRIGGER_PRICE" else "ERRORS.TRIGGERS_FORM.TAKE_PROFIT_LIMIT_MUST_BELOW_TRIGGER_PRICE", - ), - ) - } else { - null - } + OrderType.stopLimit, OrderType.takeProfitLimit -> { + parser.asDouble(parser.value(triggerOrder, "price.limitPrice")) + ?.let { limitPrice -> + parser.asDouble(parser.value(triggerOrder, "price.triggerPrice")) + ?.let { triggerPrice -> + if (side == OrderSide.buy && limitPrice < triggerPrice) { + return limitPriceError( + "LIMIT_MUST_ABOVE_TRIGGER_PRICE", + fields, + if (type == OrderType.stopLimit) { + "ERRORS.TRIGGERS_FORM_TITLE.STOP_LOSS_LIMIT_MUST_ABOVE_TRIGGER_PRICE" + } else { + "ERRORS.TRIGGERS_FORM_TITLE.TAKE_PROFIT_LIMIT_MUST_ABOVE_TRIGGER_PRICE" + }, + if (type == OrderType.stopLimit) { + "ERRORS.TRIGGERS_FORM.STOP_LOSS_LIMIT_MUST_ABOVE_TRIGGER_PRICE" + } else { + "ERRORS.TRIGGERS_FORM.TAKE_PROFIT_LIMIT_MUST_ABOVE_TRIGGER_PRICE" + }, + ) + } else if (side == OrderSide.sell && limitPrice > triggerPrice) { + return limitPriceError( + "LIMIT_MUST_BELOW_TRIGGER_PRICE", + fields, + if (type == OrderType.stopLimit) { + "ERRORS.TRIGGERS_FORM_TITLE.STOP_LOSS_LIMIT_MUST_BELOW_TRIGGER_PRICE" + } else { + "ERRORS.TRIGGERS_FORM_TITLE.TAKE_PROFIT_LIMIT_MUST_BELOW_TRIGGER_PRICE" + }, + if (type == OrderType.stopLimit) { + "ERRORS.TRIGGERS_FORM.STOP_LOSS_LIMIT_MUST_BELOW_TRIGGER_PRICE" + } else { + "ERRORS.TRIGGERS_FORM.TAKE_PROFIT_LIMIT_MUST_BELOW_TRIGGER_PRICE" + }, + ) + } else { + null } - } - } + } + } } else -> null } } + private fun limitPriceError( + errorCode: String, + fields: List?, + titleStringKey: String, + textStringKey: String, + ): List? { + return listOf( + error( + "ERROR", + errorCode, + fields, + "APP.TRADE.MODIFY_TRIGGER_PRICE", + titleStringKey, + textStringKey, + ), + ) + } + private fun validateSize( orderSize: Double?, market: Map?, @@ -543,31 +636,11 @@ internal class TriggerOrdersInputValidator( return null } - private fun requiredTriggerToIndexPrice(type: String, side: String): RelativeToPrice? { - return when (type) { - "STOP_LIMIT", "STOP_MARKET" -> - when (side) { - "BUY" -> RelativeToPrice.ABOVE - "SELL" -> RelativeToPrice.BELOW - else -> null - } - - "TAKE_PROFIT", "TAKE_PROFIT_MARKET" -> - when (side) { - "BUY" -> RelativeToPrice.BELOW - "SELL" -> RelativeToPrice.ABOVE - else -> null - } - - else -> null - } - } - private fun triggerToIndexError( triggerToIndex: RelativeToPrice, oraclePrice: Double, tickSize: String, - type: String, + type: OrderType, inputField: String?, ): Map { val action = "APP.TRADE.MODIFY_TRIGGER_PRICE" @@ -580,6 +653,7 @@ internal class TriggerOrdersInputValidator( ), ) val fields = listOfNotNull(inputField) + val isStopLoss = type == OrderType.stopLimit || type == OrderType.stopMarket return when (triggerToIndex) { RelativeToPrice.ABOVE -> error( @@ -587,8 +661,16 @@ internal class TriggerOrdersInputValidator( "TRIGGER_MUST_ABOVE_INDEX_PRICE", fields, action, - if (type == "STOP_LIMIT" || type == "STOP_MARKET") "ERRORS.TRIGGERS_FORM_TITLE.STOP_LOSS_TRIGGER_MUST_ABOVE_INDEX_PRICE" else "ERRORS.TRIGGERS_FORM_TITLE.TAKE_PROFIT_TRIGGER_MUST_ABOVE_INDEX_PRICE", - if (type == "STOP_LIMIT" || type == "STOP_MARKET") "ERRORS.TRIGGERS_FORM.STOP_LOSS_TRIGGER_MUST_ABOVE_INDEX_PRICE" else "ERRORS.TRIGGERS_FORM.TAKE_PROFIT_TRIGGER_MUST_ABOVE_INDEX_PRICE", + if (isStopLoss) { + "ERRORS.TRIGGERS_FORM_TITLE.STOP_LOSS_TRIGGER_MUST_ABOVE_INDEX_PRICE" + } else { + "ERRORS.TRIGGERS_FORM_TITLE.TAKE_PROFIT_TRIGGER_MUST_ABOVE_INDEX_PRICE" + }, + if (isStopLoss) { + "ERRORS.TRIGGERS_FORM.STOP_LOSS_TRIGGER_MUST_ABOVE_INDEX_PRICE" + } else { + "ERRORS.TRIGGERS_FORM.TAKE_PROFIT_TRIGGER_MUST_ABOVE_INDEX_PRICE" + }, params, ) @@ -597,10 +679,79 @@ internal class TriggerOrdersInputValidator( "TRIGGER_MUST_BELOW_INDEX_PRICE", fields, action, - if (type == "STOP_LIMIT" || type == "STOP_MARKET") "ERRORS.TRIGGERS_FORM_TITLE.STOP_LOSS_TRIGGER_MUST_BELOW_INDEX_PRICE" else "ERRORS.TRIGGERS_FORM_TITLE.TAKE_PROFIT_TRIGGER_MUST_BELOW_INDEX_PRICE", - if (type == "STOP_LIMIT" || type == "STOP_MARKET") "ERRORS.TRIGGERS_FORM.STOP_LOSS_TRIGGER_MUST_BELOW_INDEX_PRICE" else "ERRORS.TRIGGERS_FORM.TAKE_PROFIT_TRIGGER_MUST_BELOW_INDEX_PRICE", + if (isStopLoss) { + "ERRORS.TRIGGERS_FORM_TITLE.STOP_LOSS_TRIGGER_MUST_BELOW_INDEX_PRICE" + } else { + "ERRORS.TRIGGERS_FORM_TITLE.TAKE_PROFIT_TRIGGER_MUST_BELOW_INDEX_PRICE" + }, + if (isStopLoss) { + "ERRORS.TRIGGERS_FORM.STOP_LOSS_TRIGGER_MUST_BELOW_INDEX_PRICE" + } else { + "ERRORS.TRIGGERS_FORM.TAKE_PROFIT_TRIGGER_MUST_BELOW_INDEX_PRICE" + }, params, ) } } } + +private fun isOrderIncludedInEquityTierLimit( + orderType: OrderType, + timeInForce: OrderTimeInForce, + status: OrderStatus +): Boolean { + val isCurrentOrderStateful = isStatefulOrder(orderType, timeInForce) + // Short term with IOC or FOK should not be counted + val isShortTermAndRequiresImmediateExecution = + !isCurrentOrderStateful && (timeInForce == OrderTimeInForce.IOC || timeInForce == OrderTimeInForce.FOK) + + return if (!isShortTermAndRequiresImmediateExecution) { + when (status) { + OrderStatus.open, OrderStatus.pending, OrderStatus.untriggered, OrderStatus.partiallyFilled -> true + else -> false + } + } else { + false + } +} + +private fun isStatefulOrder(orderType: OrderType, timeInForce: OrderTimeInForce): Boolean { + return when (orderType) { + OrderType.market -> false + OrderType.limit -> { + when (timeInForce) { + OrderTimeInForce.GTT -> true + else -> false + } + } + else -> true + } +} + +private fun requiredTriggerToLiquidationPrice(type: OrderType?, side: OrderSide): RelativeToPrice? { + return when (type) { + OrderType.stopMarket -> + when (side) { + OrderSide.buy -> RelativeToPrice.BELOW + OrderSide.sell -> RelativeToPrice.ABOVE + } + else -> null + } +} + +private fun requiredTriggerToIndexPrice(type: OrderType, side: OrderSide): RelativeToPrice? { + return when (type) { + OrderType.stopLimit, OrderType.stopMarket -> + when (side) { + OrderSide.buy -> RelativeToPrice.ABOVE + OrderSide.sell -> RelativeToPrice.BELOW + } + + OrderType.takeProfitLimit, OrderType.takeProfitMarket -> + when (side) { + OrderSide.buy -> RelativeToPrice.BELOW + OrderSide.sell -> RelativeToPrice.ABOVE + } + else -> null + } +} diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeInputDataValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeInputDataValidator.kt index bfab29e4a..69ebfc2d7 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeInputDataValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeInputDataValidator.kt @@ -10,6 +10,7 @@ import exchange.dydx.abacus.utils.Rounder import exchange.dydx.abacus.validator.BaseInputValidator import exchange.dydx.abacus.validator.PositionChange import exchange.dydx.abacus.validator.TradeValidatorProtocol +import exchange.dydx.abacus.validator.trade.SubaccountLimitConstants.MAX_NUM_OPEN_UNTRIGGERED_ORDERS import kotlin.time.Duration.Companion.days /* @@ -35,6 +36,10 @@ internal data class EquityTier( var nextLevelRequiredTotalNetCollateralUSD: Double? = null } +internal object SubaccountLimitConstants { + const val MAX_NUM_OPEN_UNTRIGGERED_ORDERS = 20 +} + internal class TradeInputDataValidator( localizer: LocalizerProtocol?, formatter: Formatter?, @@ -42,9 +47,6 @@ internal class TradeInputDataValidator( ) : BaseInputValidator(localizer, formatter, parser), TradeValidatorProtocol { - @Suppress("PropertyName") - private val MAX_NUM_OPEN_UNTRIGGERED_ORDERS: Int = 20 - override fun validateTrade( subaccount: Map?, market: Map?, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeTriggerPriceValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeTriggerPriceValidator.kt index 705707fb8..b4b985dc5 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeTriggerPriceValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeTriggerPriceValidator.kt @@ -103,6 +103,7 @@ internal class TradeTriggerPriceValidator( triggerToLiquidationError( triggerToLiquidation, liquidationPrice, + tickSize, ), ) } @@ -114,6 +115,7 @@ internal class TradeTriggerPriceValidator( triggerToLiquidationError( triggerToLiquidation, liquidationPrice, + tickSize, ), ) } @@ -234,12 +236,19 @@ internal class TradeTriggerPriceValidator( private fun triggerToLiquidationError( triggerToLiquidation: RelativeToPrice, triggerLiquidation: Double, + tickSize: String, ): Map { val fields = listOf("price.triggerPrice") val action = "APP.TRADE.MODIFY_TRIGGER_PRICE" // Localizations uses TRIGGER_PRICE_LIMIT as paramater name val params = - mapOf("TRIGGER_PRICE_LIMIT" to mapOf("value" to triggerLiquidation, "format" to "price")) + mapOf( + "TRIGGER_PRICE_LIMIT" to mapOf( + "value" to triggerLiquidation, + "format" to "price", + "tickSize" to tickSize, + ), + ) return when (triggerToLiquidation) { RelativeToPrice.ABOVE -> error( "ERROR", diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/validation/TriggerOrdersInputValidationTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/validation/TriggerOrdersInputValidationTests.kt index 48204c6a9..df69f010b 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/validation/TriggerOrdersInputValidationTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/validation/TriggerOrdersInputValidationTests.kt @@ -116,6 +116,42 @@ class TriggerOrdersInputValidationTests : V4BaseTests() { perp.triggerOrders("STOP_MARKET", TriggerOrdersInputField.stopLossOrderType, 0) }, null) + test( + { + perp.triggerOrders("900", TriggerOrdersInputField.stopLossPrice, 0) + }, + """ + { + "input": { + "current": "triggerOrders", + "triggerOrders": { + "stopLossOrder": { + "type": "STOP_MARKET" + } + }, + "errors": [ + { + "type": "ERROR", + "code": "SELL_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE", + "fields": ["stopLossOrder.price.triggerPrice"], + "resources": { + "title": { + "stringKey": "ERRORS.TRIGGERS_FORM_TITLE.SELL_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE" + }, + "text": { + "stringKey": "ERRORS.TRIGGERS_FORM.SELL_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE_NO_LIMIT" + }, + "action": { + "stringKey": "APP.TRADE.MODIFY_TRIGGER_PRICE" + } + } + } + ] + } + } + """.trimIndent(), + ) + test( { perp.triggerOrders("2000", TriggerOrdersInputField.stopLossPrice, 0) @@ -238,6 +274,34 @@ class TriggerOrdersInputValidationTests : V4BaseTests() { """.trimIndent(), ) + test({ + perp.triggerOrders("800", TriggerOrdersInputField.stopLossLimitPrice, 0) + }, null) + + test( + { + perp.triggerOrders("900", TriggerOrdersInputField.stopLossPrice, 0) + }, + """ + { + "input": { + "current": "triggerOrders", + "triggerOrders": { + "stopLossOrder": { + "type": "STOP_LIMIT", + "side": "SELL", + "price": { + "triggerPrice": "900", + "limitPrice": "800" + } + } + }, + "errors": null + } + } + """.trimIndent(), + ) + test( { perp.triggerOrders("1000", TriggerOrdersInputField.stopLossPrice, 0) diff --git a/v4_abacus.podspec b/v4_abacus.podspec index b15605f10..76d91e713 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.6.47' + spec.version = '1.6.48' spec.homepage = 'https://github.com/dydxprotocol/v4-abacus' spec.source = { :http=> ''} spec.authors = ''