diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/AsyncAbacusStateManagerProtocol.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/AsyncAbacusStateManagerProtocol.kt index d93103864..3524986e4 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/AsyncAbacusStateManagerProtocol.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/AsyncAbacusStateManagerProtocol.kt @@ -79,7 +79,7 @@ interface AsyncAbacusStateManagerProtocol { // Commit changes with params fun faucet(amount: Double, callback: TransactionCallback) - fun cancelOrder(orderId: String, callback: TransactionCallback) + fun cancelOrder(orderId: String, callback: TransactionCallback, isOrphanedTriggerOrder: Boolean) // Bridge functions. // If client is not using cancelOrder function, it should call orderCanceled function with diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/manager/AsyncAbacusStateManagerV2.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/manager/AsyncAbacusStateManagerV2.kt index 60017b739..3f21874eb 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/manager/AsyncAbacusStateManagerV2.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/manager/AsyncAbacusStateManagerV2.kt @@ -592,9 +592,9 @@ class AsyncAbacusStateManagerV2( } } - override fun cancelOrder(orderId: String, callback: TransactionCallback) { + override fun cancelOrder(orderId: String, callback: TransactionCallback, isOrphanedTriggerOrder: Boolean) { try { - adaptor?.cancelOrder(orderId, callback) + adaptor?.cancelOrder(orderId, callback, isOrphanedTriggerOrder) } catch (e: Exception) { val error = V4TransactionErrors.error(null, e.toString()) callback(false, error, null) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/manager/StateManagerAdaptorV2.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/manager/StateManagerAdaptorV2.kt index 73119fe49..07d0821a5 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/manager/StateManagerAdaptorV2.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/manager/StateManagerAdaptorV2.kt @@ -581,8 +581,8 @@ internal class StateManagerAdaptorV2( accounts.stopWatchingLastOrder() } - internal fun cancelOrder(orderId: String, callback: TransactionCallback) { - accounts.cancelOrder(orderId, callback) + internal fun cancelOrder(orderId: String, callback: TransactionCallback, isOrphanedTriggerOrder: Boolean) { + accounts.cancelOrder(orderId, callback, isOrphanedTriggerOrder) } internal fun orderCanceled(orderId: String) { diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/AccountSupervisor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/AccountSupervisor.kt index 90e5ab7c0..ce91b267c 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/AccountSupervisor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/AccountSupervisor.kt @@ -1121,8 +1121,8 @@ internal fun AccountSupervisor.faucet(amount: Double, callback: TransactionCallb subaccount?.faucet(amount, callback) } -internal fun AccountSupervisor.cancelOrder(orderId: String, callback: TransactionCallback) { - subaccount?.cancelOrder(orderId, callback) +internal fun AccountSupervisor.cancelOrder(orderId: String, callback: TransactionCallback, isOrphanedTriggerOrder: Boolean) { + subaccount?.cancelOrder(orderId, callback, isOrphanedTriggerOrder) } internal fun AccountSupervisor.orderCanceled(orderId: String) { diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/AccountsSupervisor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/AccountsSupervisor.kt index 7d94d9ab7..219ecebeb 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/AccountsSupervisor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/AccountsSupervisor.kt @@ -333,8 +333,8 @@ internal fun AccountsSupervisor.faucet(amount: Double, callback: TransactionCall account?.faucet(amount, callback) } -internal fun AccountsSupervisor.cancelOrder(orderId: String, callback: TransactionCallback) { - account?.cancelOrder(orderId, callback) +internal fun AccountsSupervisor.cancelOrder(orderId: String, callback: TransactionCallback, isOrphanedTriggerOrder: Boolean) { + account?.cancelOrder(orderId, callback, isOrphanedTriggerOrder) } internal fun AccountsSupervisor.orderCanceled(orderId: String) { diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/SubaccountSupervisor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/SubaccountSupervisor.kt index 0cbf4762d..b20f2c1c1 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/SubaccountSupervisor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/SubaccountSupervisor.kt @@ -3,10 +3,12 @@ package exchange.dydx.abacus.state.v2.supervisor import abs import exchange.dydx.abacus.calculator.TriggerOrdersConstants.TRIGGER_ORDER_DEFAULT_DURATION_DAYS import exchange.dydx.abacus.output.Notification +import exchange.dydx.abacus.output.PositionSide import exchange.dydx.abacus.output.SubaccountOrder import exchange.dydx.abacus.output.TransferRecordType import exchange.dydx.abacus.output.input.IsolatedMarginAdjustmentType import exchange.dydx.abacus.output.input.MarginMode +import exchange.dydx.abacus.output.input.OrderSide import exchange.dydx.abacus.output.input.OrderStatus import exchange.dydx.abacus.output.input.OrderType import exchange.dydx.abacus.output.input.TradeInputGoodUntil @@ -395,6 +397,38 @@ internal class SubaccountSupervisor( } } + private var cancelingOrphanedTriggerOrders = mutableSetOf() + + private fun cancelTriggerOrder(orderId: String) { + cancelingOrphanedTriggerOrders.add(orderId) + cancelOrder(orderId, { _, _, _ -> cancelingOrphanedTriggerOrders.remove(orderId) }, true) + } + + private fun cancelTriggerOrdersWithClosedOrFlippedPositions() { + val subaccount = stateMachine.state?.subaccount(subaccountNumber) ?: return + val cancelableTriggerOrders = subaccount.orders?.filter { order -> + val isConditionalOrder = order.orderFlags == 32 + val isReduceOnly = order.reduceOnly + val isActiveOrder = + (order.status === OrderStatus.untriggered || order.status === OrderStatus.open) + isConditionalOrder && isReduceOnly && isActiveOrder + } ?: return + + cancelableTriggerOrders.forEach { order -> + if (order.id !in cancelingOrphanedTriggerOrders) { + val marketPosition = subaccount.openPositions?.find { position -> position.id === order.marketId } + val hasPositionFlippedOrClosed = marketPosition?.let { position -> + when (position.side.current) { + PositionSide.LONG -> order.side == OrderSide.buy + PositionSide.SHORT -> order.side == OrderSide.sell + else -> true + } + } ?: true + if (hasPositionFlippedOrClosed) cancelTriggerOrder(order.id) + } + } + } + private fun fromSlTpDialogParams(fromSlTpDialog: Boolean): IMap { return iMapOf( "fromSlTpDialog" to fromSlTpDialog, @@ -778,7 +812,8 @@ internal class SubaccountSupervisor( payload: HumanReadableCancelOrderPayload, analyticsPayload: IMap?, uiClickTimeMs: Double, - isTriggerOrder: Boolean = false, + fromSlTpDialog: Boolean = false, + isOrphanedTriggerOrder: Boolean = false, ): HumanReadableCancelOrderPayload { val clientId = payload.clientId val string = Json.encodeToString(payload) @@ -801,7 +836,7 @@ internal class SubaccountSupervisor( subaccountNumber, clientId, submitTimeMs, - fromSlTpDialog = isTriggerOrder, + fromSlTpDialog, ), ) } @@ -820,7 +855,7 @@ internal class SubaccountSupervisor( helper.send( error, callback, - if (isTriggerOrder) { + if (fromSlTpDialog) { HumanReadableTriggerOrdersPayload( marketId, positionSize, @@ -863,7 +898,7 @@ internal class SubaccountSupervisor( return submitPlaceOrder(callback, payload, analyticsPayload, uiClickTimeMs) } - internal fun cancelOrder(orderId: String, callback: TransactionCallback): HumanReadableCancelOrderPayload { + internal fun cancelOrder(orderId: String, callback: TransactionCallback, isOrphanedTriggerOrder: Boolean = false): HumanReadableCancelOrderPayload { val payload = cancelOrderPayload(orderId) val subaccount = stateMachine.state?.subaccount(subaccountNumber) val existingOrder = subaccount?.orders?.firstOrNull { it.id == orderId } ?: throw ParsingException( @@ -871,10 +906,10 @@ internal class SubaccountSupervisor( "no existing order to be cancelled for $orderId", ) val marketId = existingOrder.marketId - val analyticsPayload = analyticsUtils.cancelOrderAnalyticsPayload(payload, existingOrder, fromSlTpDialog = false) + val analyticsPayload = analyticsUtils.cancelOrderAnalyticsPayload(payload, existingOrder, fromSlTpDialog = false, isOrphanedTriggerOrder) val uiClickTimeMs = trackOrderClick(analyticsPayload, AnalyticsEvent.TradeCancelOrderClick) - return submitCancelOrder(orderId, marketId, callback, payload, analyticsPayload, uiClickTimeMs) + return submitCancelOrder(orderId, marketId, callback, payload, analyticsPayload, uiClickTimeMs, false, isOrphanedTriggerOrder) } internal fun commitTriggerOrders( @@ -1424,6 +1459,7 @@ internal class SubaccountSupervisor( } if (changes.changes.contains(Changes.subaccount)) { parseOrdersToMatchPlaceOrdersAndCancelOrders() + cancelTriggerOrdersWithClosedOrFlippedPositions() } } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/utils/AnalyticsUtils.kt b/src/commonMain/kotlin/exchange.dydx.abacus/utils/AnalyticsUtils.kt index 5ae5970ac..c08e458d0 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/utils/AnalyticsUtils.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/utils/AnalyticsUtils.kt @@ -179,9 +179,10 @@ class AnalyticsUtils { payload: HumanReadableCancelOrderPayload, existingOrder: SubaccountOrder?, fromSlTpDialog: Boolean? = false, + isOrphanedTriggerOrder: Boolean = false, ): IMap? { return ParsingHelper.merge( - formatCancelOrderPayload(payload, fromSlTpDialog), + formatCancelOrderPayload(payload, fromSlTpDialog, isOrphanedTriggerOrder), if (existingOrder != null) formatOrder(existingOrder) else mapOf(), )?.toIMap() } @@ -194,9 +195,11 @@ class AnalyticsUtils { private fun formatCancelOrderPayload( payload: HumanReadableCancelOrderPayload, fromSlTpDialog: Boolean? = false, + isOrphanedTriggerOrder: Boolean = false, ): IMap? { return iMapOf( "fromSlTpDialog" to fromSlTpDialog, + "isAutomaticallyCanceledByFrontend" to isOrphanedTriggerOrder, "subaccountNumber" to payload.subaccountNumber, "clientId" to payload.clientId, "orderId" to payload.orderId,