Skip to content

Commit

Permalink
partially canceled order status + canceling -> pending for short term…
Browse files Browse the repository at this point in the history
… orders (#447)

Co-authored-by: mobile-build-bot-git <[email protected]>
  • Loading branch information
aforaleka and mobile-build-bot authored Jun 17, 2024
1 parent 256085e commit 3783479
Show file tree
Hide file tree
Showing 10 changed files with 231 additions and 38 deletions.
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ allprojects {
}

group = "exchange.dydx.abacus"
version = "1.7.76"
version = "1.7.77"

repositories {
google()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -707,9 +707,10 @@ enum class OrderStatus(val rawValue: String) {
canceling("BEST_EFFORT_CANCELED"),
filled("FILLED"),
`open`("OPEN"),
pending("PENDING"),
pending("PENDING"), // indexer returns order as BEST_EFFORT_OPENED, or BEST_EFFORT_CANCELED when order is IOC
untriggered("UNTRIGGERED"),
partiallyFilled("PARTIALLY_FILLED");
partiallyFilled("PARTIALLY_FILLED"), // indexer returns order as OPEN but order is partially filled
partiallyCanceled("PARTIALLY_CANCELED"); // indexer returns order as CANCELED but order is partially filled

companion object {
operator fun invoke(rawValue: String): OrderStatus? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ internal class OrderProcessor(parser: ParserProtocol) : BaseProcessor(parser) {
"BEST_EFFORT_CANCELED" to "APP.TRADE.CANCELING",
"UNTRIGGERED" to "APP.TRADE.UNTRIGGERED",
"PARTIALLY_FILLED" to "APP.TRADE.PARTIALLY_FILLED",
"PARTIALLY_CANCELED" to "APP.TRADE.PARTIALLY_FILLED",
)
private val timeInForceStringKeys = mapOf(
"FOK" to "APP.TRADE.FILL_OR_KILL",
Expand Down Expand Up @@ -180,14 +181,13 @@ internal class OrderProcessor(parser: ParserProtocol) : BaseProcessor(parser) {
return false
}
// If updatedAt and totalFilled are the same, we use status for best guess
return when (parser.asString(existing?.get("status"))) {
"FILLED", "CANCELED" -> false
return when (val status = parser.asString(existing?.get("status"))) {
"BEST_EFFORT_CANCELED" -> {
val newStatus = parser.asString(payload["status"])
(newStatus == "FILLED") || (newStatus == "CANCELED") || (newStatus == "BEST_EFFORT_CANCELED")
}

else -> true
else -> !isStatusFinalized(status)
}
}

Expand All @@ -209,27 +209,34 @@ internal class OrderProcessor(parser: ParserProtocol) : BaseProcessor(parser) {
// the v4_parent_subaccount message has subaccountNumber available but v4_orders does not
modified.safeSet("marginMode", if (this >= NUM_PARENT_SUBACCOUNTS) MarginMode.isolated.rawValue else MarginMode.cross.rawValue)
}
val size = parser.asDouble(payload["size"])
if (size != null) {
var totalFilled = parser.asDouble(payload["totalFilled"])
var remainingSize = parser.asDouble(payload["remainingSize"])
if (totalFilled != null && remainingSize == null) {
remainingSize = size - totalFilled
} else if (totalFilled == null && remainingSize != null) {
totalFilled = size - remainingSize
}
if (totalFilled != null && remainingSize != null) {
parser.asDouble(payload["size"])?.let { size ->
parser.asDouble(payload["totalFilled"])?.let { totalFilled ->
val remainingSize = size - totalFilled
modified.safeSet("totalFilled", totalFilled)
modified.safeSet("remainingSize", remainingSize)

if (totalFilled != Numeric.double.ZERO && modified["status"] == "OPEN") {
modified.safeSet("status", "PARTIALLY_FILLED")
if (totalFilled != Numeric.double.ZERO && remainingSize != Numeric.double.ZERO) {
when (modified["status"]) {
"OPEN" -> modified.safeSet("status", "PARTIALLY_FILLED")
"CANCELED" -> modified.safeSet("status", "PARTIALLY_CANCELED") // finalized state
}
}
}
}

modified.safeSet("cancelReason", payload["removalReason"] ?: payload["cancelReason"])

parser.asDouble(payload["orderFlags"])?.let { orderFlags ->
// if order is short-term order and indexer returns best effort canceled and has no partial fill
// treat as a pending order until it's partially filled or finalized
val isShortTermOrder = orderFlags.equals(Numeric.double.ZERO)
val isBestEffortCanceled = modified["status"] == "BEST_EFFORT_CANCELED"
val cancelReason = parser.asString(modified["cancelReason"])
val isUserCanceled = cancelReason == "USER_CANCELED" || cancelReason == "ORDER_REMOVAL_REASON_USER_CANCELED"
if (isShortTermOrder && isBestEffortCanceled && !isUserCanceled) {
modified.safeSet("status", "PENDING")
}
}

modified.safeSet(
"type",
OrderTypeProcessor.orderType(
Expand Down Expand Up @@ -286,12 +293,22 @@ internal class OrderProcessor(parser: ParserProtocol) : BaseProcessor(parser) {
): Pair<Map<String, Any>, Boolean> {
if (height != null) {
when (val status = parser.asString(existing["status"])) {
"BEST_EFFORT_CANCELED" -> {
"PENDING", "BEST_EFFORT_CANCELED", "PARTIALLY_FILLED" -> {
val goodTilBlock = parser.asInt(existing["goodTilBlock"])
if (goodTilBlock != null && goodTilBlock != 0 && height.block >= goodTilBlock) {
val modified = existing.mutable();
modified["status"] = "CANCELED"
modified["updatedAt"] = height.time

parser.asDouble(existing["size"])?.let { size ->
parser.asDouble(existing["totalFilled"])?.let { totalFilled ->
val remainingSize = size - totalFilled
if (totalFilled != Numeric.double.ZERO && remainingSize != Numeric.double.ZERO) {
modified["status"] = "PARTIALLY_CANCELED"
}
}
}

updateResource(modified)
return Pair(modified, true)
}
Expand All @@ -303,11 +320,21 @@ internal class OrderProcessor(parser: ParserProtocol) : BaseProcessor(parser) {
return Pair(existing, false)
}

private fun isStatusFinalized(status: String?): Boolean {
// once an order is filled, canceled, or canceled with partial fill
// there is no need to update status again
return when (status) {
"FILLED", "CANCELED", "PARTIALLY_CANCELED" -> true
else -> false
}
}

internal fun canceled(
existing: Map<String, Any>,
): Map<String, Any> {
val modified = existing.mutable()
if (modified["status"] !== "CANCELED" && modified["status"] !== "FILLED") {
// show order status as canceling if frontend initiated cancel
if (!isStatusFinalized(parser.asString(modified["status"]))) {
modified["status"] = "BEST_EFFORT_CANCELED"
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ enum class AnalyticsEvent(val rawValue: String) {
TradePlaceOrderStatusPending("TradePlaceOrderStatusPending"),
TradePlaceOrderStatusUntriggered("TradePlaceOrderStatusUntriggered"),
TradePlaceOrderStatusPartiallyFilled("TradePlaceOrderStatusPartiallyFilled"),
TradePlaceOrderStatusPartiallyCanceled("TradePlaceOrderStatusPartiallyCanceled"),

// Trigger Order
TriggerOrderClick("TriggerOrderClick"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ class FillsNotificationProvider(
uiImplementations.localizer?.localize("NOTIFICATIONS.ORDER_FILL.TITLE")
}

OrderStatus.partiallyFilled -> {
OrderStatus.partiallyFilled, OrderStatus.partiallyCanceled -> {
uiImplementations.localizer?.localize("NOTIFICATIONS.ORDER_PARTIAL_FILL.TITLE")
}

Expand All @@ -278,7 +278,7 @@ class FillsNotificationProvider(
)
}

OrderStatus.partiallyFilled -> {
OrderStatus.partiallyFilled, OrderStatus.partiallyCanceled -> {
uiImplementations.localizer?.localize(
"NOTIFICATIONS.ORDER_PARTIAL_FILL.BODY",
paramsAsJson,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -354,13 +354,14 @@ internal class SubaccountSupervisor(
OrderStatus.pending -> AnalyticsEvent.TradePlaceOrderStatusPending
OrderStatus.untriggered -> AnalyticsEvent.TradePlaceOrderStatusUntriggered
OrderStatus.partiallyFilled -> AnalyticsEvent.TradePlaceOrderStatusPartiallyFilled
OrderStatus.partiallyCanceled -> AnalyticsEvent.TradePlaceOrderStatusPartiallyCanceled
}

tracking(orderStatusChangeEvent.rawValue, analyticsPayload)

when (order.status) {
// order reaches final state, can remove / skip further tracking
OrderStatus.cancelled, OrderStatus.filled -> {
OrderStatus.cancelled, OrderStatus.partiallyCanceled, OrderStatus.filled -> {
placeOrderRecords.remove(placeOrderRecord)
}
else -> {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,6 @@ class V3PerpTests : V3BaseTests() {
"marketId": "ETH-USD",
"price": 1500.0,
"size": 0.1,
"remainingSize": 0.1,
"createdAt": "2022-08-01T22:25:31.111Z",
"expiresAt": "2022-08-29T22:45:30.776Z",
"postOnly": false,
Expand Down Expand Up @@ -317,7 +316,6 @@ class V3PerpTests : V3BaseTests() {
"marketId": "ETH-USD",
"price": 1702.7,
"size": 45.249,
"remainingSize": 0.0,
"createdAt": "2022-08-01T19:53:29.653Z",
"expiresAt": "2022-08-01T20:13:29.361Z",
"postOnly": false,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package exchange.dydx.abacus.payload.v4

import exchange.dydx.abacus.payload.BaseTests
import exchange.dydx.abacus.responses.StateResponse
import exchange.dydx.abacus.state.app.adaptors.AbUrl
import exchange.dydx.abacus.state.manager.BlockAndTime
Expand Down Expand Up @@ -53,6 +52,8 @@ class V4AccountTests : V4BaseTests() {

testBatchedSubaccountChanged()

testPartiallyFilledAndCanceledOrders()

testEquityTiers()

testFeeTiers()
Expand Down Expand Up @@ -701,7 +702,7 @@ class V4AccountTests : V4BaseTests() {
"orders": {
"80133551-6d61-573b-9788-c1488e11027a": {
"id": "80133551-6d61-573b-9788-c1488e11027a",
"status": "BEST_EFFORT_CANCELED"
"status": "PENDING"
}
}
}
Expand Down Expand Up @@ -845,9 +846,9 @@ class V4AccountTests : V4BaseTests() {
}
""".trimIndent(),
{
val ioImplementations = BaseTests.testIOImplementations()
val localizer = BaseTests.testLocalizer(ioImplementations)
val uiImplementations = BaseTests.testUIImplementations(localizer)
val ioImplementations = testIOImplementations()
val localizer = testLocalizer(ioImplementations)
val uiImplementations = testUIImplementations(localizer)
val notificationsProvider =
NotificationsProvider(
perp,
Expand Down Expand Up @@ -877,6 +878,63 @@ class V4AccountTests : V4BaseTests() {
)
}

private fun testPartiallyFilledAndCanceledOrders() {
test(
{
perp.socket(
testWsUrl,
mock.accountsChannel.v4_parent_subaccounts_partially_filled_and_canceled_orders,
0,
BlockAndTime(14689438, Clock.System.now()),
)
},
"""
{
"wallet": {
"account": {
"subaccounts": {
"0": {
"orders": {
"a4586c75-c3f5-5bf5-877a-b3f2c8ff32a7": {
"status": "PARTIALLY_FILLED"
},
"3a8c6f8f-d8dd-54b5-a3a1-d318f586a80c": {
"status": "PARTIALLY_CANCELED"
}
}
}
}
}
}
}
""".trimIndent(),
{
val ioImplementations = testIOImplementations()
val localizer = testLocalizer(ioImplementations)
val uiImplementations = testUIImplementations(localizer)
val notificationsProvider =
NotificationsProvider(
perp,
uiImplementations,
environment = mock.v4Environment,
Parser(),
JsonEncoder(),
)
val notifications = notificationsProvider.buildNotifications(0)
assertEquals(
8,
notifications.size,
)
val order = notifications["order:3a8c6f8f-d8dd-54b5-a3a1-d318f586a80c"]
assertNotNull(order)
assertEquals(
"NOTIFICATIONS.ORDER_PARTIAL_FILL.TITLE",
order.title,
)
},
)
}

private fun testEquityTiers() {
test(
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1980,7 +1980,7 @@ class V4SubaccountTests : V4BaseTests() {
"size":"545",
"price":"2.201",
"type":"LIMIT",
"status":"BEST_EFFORT_CANCELED",
"status":"PENDING",
"timeInForce":"IOC",
"postOnly":false,
"reduceOnly":false,
Expand All @@ -1998,7 +1998,7 @@ class V4SubaccountTests : V4BaseTests() {
"size":"3540",
"price":"0.5649",
"type":"LIMIT",
"status":"BEST_EFFORT_CANCELED",
"status":"PENDING",
"timeInForce":"IOC",
"postOnly":false,
"reduceOnly":false,
Expand Down
Loading

0 comments on commit 3783479

Please sign in to comment.