diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt index 407185190..0ae7ef493 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt @@ -1,3 +1,5 @@ +@file:Suppress("ktlint:standard:property-naming") + package exchange.dydx.abacus.calculator import abs @@ -54,12 +56,13 @@ internal class TradeInputCalculator( ): Map { val account = parser.asNativeMap(state["account"]) val subaccount = if (subaccountNumber != null) { - parser.asNativeMap( - parser.value( - account, - "subaccounts.$subaccountNumber", - ), - ) + parser.asMap(parser.value(account, "groupedSubaccounts.$subaccountNumber")) + ?: parser.asNativeMap( + parser.value( + account, + "subaccounts.$subaccountNumber", + ), + ) } else { null } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+ClosePositionInput.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+ClosePositionInput.kt index 8e5ceab35..207ff7c6c 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+ClosePositionInput.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+ClosePositionInput.kt @@ -37,7 +37,7 @@ fun TradingStateMachine.closePosition( val input = this.input?.mutable() ?: mutableMapOf() input["current"] = "closePosition" val trade = - parser.asMap(input["closePosition"])?.mutable() ?: inititiateClosePosition( + parser.asMap(input["closePosition"])?.mutable() ?: initiateClosePosition( null, subaccountNumber, ) @@ -45,7 +45,7 @@ fun TradingStateMachine.closePosition( var sizeChanged = false when (typeText) { ClosePositionInputField.market.rawValue -> { - val position = if (data != null) getPosition(data) else null + val position = if (data != null) getPosition(data, subaccountNumber) else null if (position != null) { if (data != null) { if (parser.asString(trade["marketId"]) != data) { @@ -100,14 +100,21 @@ fun TradingStateMachine.closePosition( fun TradingStateMachine.getPosition( marketId: String, - subaccountNumber: Int = 0 + subaccountNumber: Int, ): Map? { + val groupedSubaccounts = parser.asMap(parser.value(wallet, "account.groupedSubaccounts")) + val path = if (groupedSubaccounts != null) { + "account.groupedSubaccounts.$subaccountNumber.openPositions.$marketId" + } else { + "account.subaccounts.$subaccountNumber.openPositions.$marketId" + } val position = parser.asMap( parser.value( wallet, - "account.subaccounts.$subaccountNumber.openPositions.$marketId", + path, ), ) + return if (position != null && ( parser.asDouble(parser.value(position, "size.current")) ?: Numeric.double.ZERO diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt index dc08a7dac..7f8463b3d 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt @@ -164,7 +164,7 @@ internal fun TradingStateMachine.initiateTrade( return parser.asMap(modified["trade"])?.mutable() ?: trade } -internal fun TradingStateMachine.inititiateClosePosition( +internal fun TradingStateMachine.initiateClosePosition( marketId: String?, subaccountNumber: Int, ): MutableMap { diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt index 4e11626d4..e0cd84253 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt @@ -1198,7 +1198,7 @@ open class TradingStateMachine( for (subaccountNumber in subaccountNumbers) { val subaccountText = "$subaccountNumber" val subaccount = - parser.asNativeMap(parser.value(this.account, "subaccounts.$subaccountNumber")) + parser.asNativeMap(parser.value(this.account, "groupedSubaccounts.$subaccountNumber")) ?: parser.asNativeMap(parser.value(this.account, "subaccounts.$subaccountNumber")) if (changes.changes.contains(Changes.historicalPnl)) { val now = ServerTime.now() 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 63bef5cf2..3acf36469 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 @@ -1169,8 +1169,10 @@ internal class SubaccountSupervisor( val goodTilTimeInSeconds = null val goodTilBlock = 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, clientId, "MARKET", diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/app/manager/v2/V4TransactionTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/app/manager/v2/V4TransactionTests.kt index b004ab9ea..6810cc8c6 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/app/manager/v2/V4TransactionTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/app/manager/v2/V4TransactionTests.kt @@ -10,6 +10,7 @@ import exchange.dydx.abacus.protocols.TransactionCallback import exchange.dydx.abacus.state.manager.HumanReadablePlaceOrderPayload import exchange.dydx.abacus.state.manager.HumanReadableTriggerOrdersPayload import exchange.dydx.abacus.state.manager.setAddresses +import exchange.dydx.abacus.state.model.ClosePositionInputField import exchange.dydx.abacus.state.model.TradeInputField import exchange.dydx.abacus.state.model.TriggerOrdersInputField import exchange.dydx.abacus.state.v2.manager.AsyncAbacusStateManagerV2 @@ -344,19 +345,26 @@ class V4TransactionTests : NetworkTests() { assertTransactionQueueEmpty() } - private fun setStateMachineForIsolatedMarginTests(stateManager: AsyncAbacusStateManagerV2) { + private fun setStateMachineForIsolatedMarginTests(stateManager: AsyncAbacusStateManagerV2, withPositions: Boolean = false) { stateManager.readyToConnect = true testWebSocket?.simulateConnected(true) testWebSocket?.simulateReceived(mock.connectionMock.connectedMessage) testWebSocket?.simulateReceived(mock.marketsChannel.v4_subscribed_r1) stateManager.setAddresses(null, "dydx155va0m7wz5n8zcqscn9afswwt04n4usj46wvp5") - testWebSocket?.simulateReceived(mock.v4ParentSubaccountsMock.subscribed) - testWebSocket?.simulateReceived(mock.v4ParentSubaccountsMock.channel_batch_data) - + if (withPositions) { + testWebSocket?.simulateReceived(mock.v4ParentSubaccountsMock.subscribed_with_positions) + } else { + testWebSocket?.simulateReceived(mock.v4ParentSubaccountsMock.subscribed) + } stateManager.market = "BTC-USD" } + private fun prepareIsolatedMarginClosePosition() { + stateManager.closePosition("APE-USD", ClosePositionInputField.market) + stateManager.closePosition("1", ClosePositionInputField.percent) + } + private fun prepareIsolatedMarginTrade(isShortTerm: Boolean) { stateManager.trade("2000", TradeInputField.limitPrice) stateManager.trade("0.01", TradeInputField.size) @@ -371,6 +379,15 @@ class V4TransactionTests : NetworkTests() { } } + @Test + fun testIsolatedMarginClosePosition() { + setStateMachineForIsolatedMarginTests(stateManager, withPositions = true) + prepareIsolatedMarginClosePosition() + val closePositionPayload = subaccountSupervisor?.closePositionPayload(0) + assertNotNull(closePositionPayload, "Close position payload should not be null") + assertEquals(128, closePositionPayload.subaccountNumber) + } + @Test fun testIsolatedMarginPlaceOrderTransactions() { setStateMachineForIsolatedMarginTests(stateManager) diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/tests/payloads/MarketsChannelMock.kt b/src/commonTest/kotlin/exchange.dydx.abacus/tests/payloads/MarketsChannelMock.kt index 101e7c7d4..19289b9bc 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/tests/payloads/MarketsChannelMock.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/tests/payloads/MarketsChannelMock.kt @@ -29,25 +29,25 @@ internal class MarketsChannelMock { "stepBaseQuantums": 1000000, "subticksPerTick": 1000000 }, - "LDO-USD":{ - "clobPairId":"26", - "ticker":"LDO-USD", - "status":"ACTIVE", - "oraclePrice":"1.619032989", - "priceChange24H":"-0.049527727", - "volume24H":"1181661.046", - "trades24H":2992, - "nextFundingRate":"0", - "initialMarginFraction":"0.2", - "maintenanceMarginFraction":"0.1", - "openInterest":"83181", - "atomicResolution":-6, - "quantumConversionExponent":-9, - "tickSize":"0.001", - "stepSize":"1", - "stepBaseQuantums":1000000, - "subticksPerTick":1000000 - }, + "LDO-USD":{ + "clobPairId":"26", + "ticker":"LDO-USD", + "status":"ACTIVE", + "oraclePrice":"1.619032989", + "priceChange24H":"-0.049527727", + "volume24H":"1181661.046", + "trades24H":2992, + "nextFundingRate":"0", + "initialMarginFraction":"0.2", + "maintenanceMarginFraction":"0.1", + "openInterest":"83181", + "atomicResolution":-6, + "quantumConversionExponent":-9, + "tickSize":"0.001", + "stepSize":"1", + "stepBaseQuantums":1000000, + "subticksPerTick":1000000 + }, "BTC-USD": { "clobPairId":"0", "ticker":"BTC-USD", @@ -2739,6 +2739,25 @@ internal class MarketsChannelMock { "channel":"v4_markets", "contents":{ "markets":{ + "APE-USD": { + "clobPairId": "22", + "ticker": "APE-USD", + "status": "ACTIVE", + "oraclePrice": "1.260519794", + "priceChange24H": "-0.013680206", + "volume24H": "519465.935", + "trades24H": 1358, + "nextFundingRate": "-0.00000426818181818182", + "initialMarginFraction": "0.2", + "maintenanceMarginFraction": "0.1", + "openInterest": "142671", + "atomicResolution": -6, + "quantumConversionExponent": -9, + "tickSize": "0.001", + "stepSize": "1", + "stepBaseQuantums": 1000000, + "subticksPerTick": 1000000 + }, "BTC-USD":{ "clobPairId":"0", "ticker":"BTC-USD", diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/tests/payloads/V4ParentSubaccountsMock.kt b/src/commonTest/kotlin/exchange.dydx.abacus/tests/payloads/V4ParentSubaccountsMock.kt index cb2ac7522..0fe019d39 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/tests/payloads/V4ParentSubaccountsMock.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/tests/payloads/V4ParentSubaccountsMock.kt @@ -111,6 +111,156 @@ internal class V4ParentSubaccountsMock { } """.trimIndent() + internal val subscribed_with_positions = """ + { + "type": "subscribed", + "connection_id": "3adc59d6-09cb-432e-b987-ced0da32bec9", + "message_id": 2, + "channel": "v4_parent_subaccounts", + "id": "dydx155va0m7wz5n8zcqscn9afswwt04n4usj46wvp5/0", + "contents": { + "subaccount": { + "address": "dydx155va0m7wz5n8zcqscn9afswwt04n4usj46wvp5", + "parentSubaccountNumber": 0, + "equity": "1004.026896843", + "freeCollateral": "963.0237707731", + "childSubaccounts": [ + { + "address": "dydx155va0m7wz5n8zcqscn9afswwt04n4usj46wvp5", + "subaccountNumber": 0, + "equity": "862.675984", + "freeCollateral": "862.675984", + "openPerpetualPositions": {}, + "assetPositions": { + "USDC": { + "size": "862.675984", + "symbol": "USDC", + "side": "LONG", + "assetId": "0", + "subaccountNumber": 0 + } + }, + "marginEnabled": true, + "updatedAtHeight": "12906107" + }, + { + "address": "dydx155va0m7wz5n8zcqscn9afswwt04n4usj46wvp5", + "subaccountNumber": 128, + "equity": "59.810279856", + "freeCollateral": "57.7685502848", + "openPerpetualPositions": { + "APE-USD": { + "market": "APE-USD", + "status": "OPEN", + "side": "LONG", + "size": "8", + "maxSize": "8", + "entryPrice": "1.292", + "exitPrice": null, + "realizedPnl": "0", + "unrealizedPnl": "-0.127352144", + "createdAt": "2024-05-22T20:18:18.080Z", + "createdAtHeight": "12905490", + "closedAt": null, + "sumOpen": "8", + "sumClose": "0", + "netFunding": "0", + "subaccountNumber": 128 + } + }, + "assetPositions": { + "USDC": { + "size": "49.601632", + "symbol": "USDC", + "side": "LONG", + "assetId": "0", + "subaccountNumber": 128 + } + }, + "marginEnabled": true, + "updatedAtHeight": "12905505" + }, + { + "address": "dydx155va0m7wz5n8zcqscn9afswwt04n4usj46wvp5", + "subaccountNumber": 256, + "equity": "41.902552695", + "freeCollateral": "21.9040510255", + "openPerpetualPositions": { + "XLM-USD": { + "market": "XLM-USD", + "status": "OPEN", + "side": "LONG", + "size": "1810", + "maxSize": "1810", + "entryPrice": "0.11046795580110497238", + "exitPrice": null, + "realizedPnl": "0", + "unrealizedPnl": "0.0380166949999999922", + "createdAt": "2024-05-22T20:30:13.972Z", + "createdAtHeight": "12906057", + "closedAt": null, + "sumOpen": "1810", + "sumClose": "0", + "netFunding": "0", + "subaccountNumber": 256 + } + }, + "assetPositions": { + "USDC": { + "size": "158.082464", + "symbol": "USDC", + "side": "SHORT", + "assetId": "0", + "subaccountNumber": 256 + } + }, + "marginEnabled": true, + "updatedAtHeight": "12906057" + }, + { + "address": "dydx155va0m7wz5n8zcqscn9afswwt04n4usj46wvp5", + "subaccountNumber": 384, + "equity": "39.638080292", + "freeCollateral": "20.6751854628", + "openPerpetualPositions": { + "ARB-USD": { + "market": "ARB-USD", + "status": "OPEN", + "side": "LONG", + "size": "166", + "maxSize": "166", + "entryPrice": "1.14298795180722891566", + "exitPrice": null, + "realizedPnl": "0", + "unrealizedPnl": "-0.10705170799999999956", + "createdAt": "2024-05-22T20:31:23.666Z", + "createdAtHeight": "12906110", + "closedAt": null, + "sumOpen": "166", + "sumClose": "0", + "netFunding": "0", + "subaccountNumber": 384 + } + }, + "assetPositions": { + "USDC": { + "size": "149.990868", + "symbol": "USDC", + "side": "SHORT", + "assetId": "0", + "subaccountNumber": 384 + } + }, + "marginEnabled": true, + "updatedAtHeight": "12906110" + } + ] + }, + "orders": [] + } + } + """.trimIndent() + internal val channel_batch_data = """ { "type": "channel_batch_data",