Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v1.6.48: Add liquidation warning for stop market trigger price #315

Merged
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ allprojects {
}

group = "exchange.dydx.abacus"
version = "1.6.45"
version = "1.6.46"

repositories {
google()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,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(
Expand Down Expand Up @@ -87,6 +88,7 @@ internal class TriggerOrdersInputValidator(
market,
oraclePrice,
tickSize,
validateStopLossTriggerToLiquidationPrice(stopLossOrder, position, tickSize),
)
} else {
null
Expand Down Expand Up @@ -130,6 +132,7 @@ internal class TriggerOrdersInputValidator(
market: Map<String, Any>?,
oraclePrice: Double,
tickSize: String,
liquidationPriceErrors: List<Any>? = null,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might we be able to instead pass position as an argument and then move the liquidationPriceErrors calculations into this function body? Seems like that is the existing pattern for this function's body

Copy link
Contributor Author

@moo-onthelawn moo-onthelawn Apr 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm so liquidationPriceErrors only applies to stop loss orders (not take profit) - which is why I have it passed in from the caller. I could pass in a isStopLoss? as a param here but not sure if that's much better 🤔 oh actually I do the check for type anyways in the stopLossLiquidation function; so ignore me, yes cando

): MutableList<Any>? {
val triggerErrors = mutableListOf<Any>()

Expand Down Expand Up @@ -166,9 +169,84 @@ internal class TriggerOrdersInputValidator(
*/
triggerErrors.addAll(it)
}

liquidationPriceErrors?.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 validateStopLossTriggerToLiquidationPrice(
triggerOrder: Map<String, Any>,
position: Map<String, Any>,
tickSize: String,
): List<Any>? {
val liquidationPrice = parser.asDouble(parser.value(position, "liquidationPrice.current"))
val triggerPrice = parser.asDouble(parser.value(triggerOrder, "price.triggerPrice"))

if (liquidationPrice == null || triggerPrice == null) {
return null
}

val type = parser.asString(triggerOrder["type"])
val side = parser.asString(triggerOrder["side"])

return when (requiredTriggerToLiquidationPrice(type, side)) {
RelativeToPrice.ABOVE -> {
if (triggerPrice <= liquidationPrice) {
listOf(
error(
"ERROR",
"SELL_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE",
listOf(TriggerOrdersInputField.stopLossPrice.rawValue),
"APP.TRADE.MODIFY_TRIGGER_PRICE",
"ERRORS.TRADE_BOX_TITLE.SELL_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE",
"ERRORS.TRADE_BOX.SELL_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE",
mapOf(
"TRIGGER_PRICE_LIMIT" to mapOf(
"value" to liquidationPrice,
"format" to "price",
"tickSize" to tickSize,
),
),
),
)
} else {
null
}
}
RelativeToPrice.BELOW -> {
if (triggerPrice >= liquidationPrice) {
listOf(
error(
"ERROR",
"BUY_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE",
listOf(TriggerOrdersInputField.stopLossPrice.rawValue),
"APP.TRADE.MODIFY_TRIGGER_PRICE",
"ERRORS.TRADE_BOX_TITLE.BUY_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE",
"ERRORS.TRADE_BOX.BUY_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE",
mapOf(
"TRIGGER_PRICE_LIMIT" to mapOf(
"value" to liquidationPrice,
"format" to "price",
"tickSize" to tickSize,
),
),
),
)
} else {
null
}
}
else -> null
Copy link
Contributor

@prashanDYDX prashanDYDX Apr 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this else case needed...? (as in, does the compiler complain if you remove it) and if it is, i would again consider whether this is a valid state to be in and if we should just error out instead.

Copy link
Contributor

@prashanDYDX prashanDYDX Apr 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when using enum and sealed types in a when, the compiler can enforce that we are checking every single case, so we shouldn't need an else case here.

}
}

private fun validateOrderCount(
triggerOrders: Map<String, Any>,
subaccount: Map<String, Any>?,
Expand Down Expand Up @@ -543,6 +621,19 @@ internal class TriggerOrdersInputValidator(
return null
}

private fun requiredTriggerToLiquidationPrice(type: String?, side: String?): RelativeToPrice? {
return when (type) {
"STOP_MARKET" ->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we make these case enums instead of strings, we can avoid having an else -> null case and remove a source of nullability.

I would also consider whether else is even a valid state to be in. If not, we should just error() out instead of returning null. In general, we should be looking to avoid nullable types as much as possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah interesting; yeah the current pattern I've seen in the repo is to return early (return null) if we get an unexpected null type (but then detekt yelled at me because we had too many return statements in a function oops). Happy to pivot to using error() more liberally - I think this whole file could probably be revamped a bit more (i basically did the min to get it to pass detekt) - filing a tix here.

when (side) {
"BUY" -> RelativeToPrice.BELOW
"SELL" -> RelativeToPrice.ABOVE
else -> null
}

else -> null
}
}

private fun requiredTriggerToIndexPrice(type: String, side: String): RelativeToPrice? {
return when (type) {
"STOP_LIMIT", "STOP_MARKET" ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.TRADE_BOX_TITLE.SELL_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE"
},
"text": {
"stringKey": "ERRORS.TRADE_BOX.SELL_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE"
},
"action": {
"stringKey": "APP.TRADE.MODIFY_TRIGGER_PRICE"
}
}
}
]
}
}
""".trimIndent(),
)

test(
{
perp.triggerOrders("2000", TriggerOrdersInputField.stopLossPrice, 0)
Expand Down Expand Up @@ -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)
Expand Down
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.6.45'
spec.version = '1.6.46'
spec.homepage = 'https://github.com/dydxprotocol/v4-abacus'
spec.source = { :http=> ''}
spec.authors = ''
Expand Down
Loading