diff --git a/build.gradle.kts b/build.gradle.kts index ceb229a50..541edd080 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -52,7 +52,7 @@ allprojects { } group = "exchange.dydx.abacus" -version = "1.9.6" +version = "1.9.7" repositories { google() diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/SubaccountCalculatorV2.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/SubaccountCalculatorV2.kt index 0774fcf19..cceb4b4b3 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/SubaccountCalculatorV2.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/SubaccountCalculatorV2.kt @@ -297,7 +297,7 @@ internal class SubaccountCalculatorV2( market: InternalMarketState?, period: CalculationPeriod, ): Double { - val maintenanceMarginFraction = position?.calculated?.get(period)?.adjustedImf + val maintenanceMarginFraction = position?.calculated?.get(period)?.adjustedMmf val oraclePrice = market?.perpetualMarket?.oraclePrice val size = position?.calculated?.get(period)?.size diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputMarketOrderCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputMarketOrderCalculator.kt index 2d5aeaa9a..4adbb9388 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputMarketOrderCalculator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputMarketOrderCalculator.kt @@ -423,7 +423,7 @@ internal class TradeInputMarketOrderCalculator() { worstPrice: Double?, filled: Boolean, ): TradeInputMarketOrder? { - return if (size != null && usdcSize != null && size != Numeric.double.ZERO) { + return if (size != null && usdcSize != null) { TradeInputMarketOrder( orderbook = orderbook.map { OrderbookUsage( @@ -431,7 +431,7 @@ internal class TradeInputMarketOrderCalculator() { size = it.size, ) }.toIList(), - price = (usdcSize / size), + price = if (size != Numeric.double.ZERO) usdcSize / size else null, size = size, usdcSize = usdcSize, worstPrice = worstPrice, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputSummaryCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputSummaryCalculator.kt index b2aa0b8ce..1534d7315 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputSummaryCalculator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputSummaryCalculator.kt @@ -339,7 +339,7 @@ internal class TradeInputSummaryCalculator { } private fun getMultiplier(trade: InternalTradeInputState): Double { - return if (trade.side == OrderSide.Buy) Numeric.double.POSITIVE else Numeric.double.NEGATIVE + return if (trade.side == OrderSide.Sell) Numeric.double.POSITIVE else Numeric.double.NEGATIVE } private fun calculateTakerReward( diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/output/input/TradeInput.kt b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/TradeInput.kt index ff74c9291..9725a5236 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/output/input/TradeInput.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/TradeInput.kt @@ -356,7 +356,7 @@ data class TradeInputSummary( companion object { internal fun create( state: InternalTradeInputSummary?, - ): TradeInputSummary? { + ): TradeInputSummary { return TradeInputSummary( price = state?.price, payloadPrice = state?.payloadPrice, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/ClosePositionInputProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/ClosePositionInputProcessor.kt index 6d7a4029d..47b273b74 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/ClosePositionInputProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/ClosePositionInputProcessor.kt @@ -8,6 +8,7 @@ import exchange.dydx.abacus.calculator.v2.tradeinput.TradeInputCalculatorV2 import exchange.dydx.abacus.output.input.InputType import exchange.dydx.abacus.output.input.OrderSide import exchange.dydx.abacus.output.input.OrderType +import exchange.dydx.abacus.output.input.TradeInputPrice import exchange.dydx.abacus.output.input.TradeInputSize import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.responses.ParsingError @@ -21,6 +22,7 @@ import exchange.dydx.abacus.state.internalstate.InternalPerpetualPosition import exchange.dydx.abacus.state.internalstate.InternalRewardsParamsState import exchange.dydx.abacus.state.internalstate.InternalTradeInputState import exchange.dydx.abacus.state.internalstate.InternalWalletState +import exchange.dydx.abacus.state.internalstate.safeCreate import exchange.dydx.abacus.state.manager.StatsigConfig import exchange.dydx.abacus.state.model.ClosePositionInputField import exchange.dydx.abacus.utils.Numeric @@ -152,7 +154,7 @@ internal class ClosePositionInputProcessor( trade.marketId?.let { marketId -> val limitPrice = getMidMarketPrice(marketSummaryState, marketId) - trade.price = trade.price?.copy(limitPrice = limitPrice) + trade.price = TradeInputPrice.safeCreate(trade.price).copy(limitPrice = limitPrice) } } else { trade.type = OrderType.Market diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/wallet/account/AssetPositionsProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/wallet/account/AssetPositionsProcessor.kt index cd075f100..abe0e984e 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/wallet/account/AssetPositionsProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/wallet/account/AssetPositionsProcessor.kt @@ -36,7 +36,7 @@ internal class AssetPositionsProcessor( payload: List? ): Map? { return if (payload != null) { - var modified = existing?.mutable() ?: mutableMapOf() + val modified = existing?.mutable() ?: mutableMapOf() for (item in payload) { val assetPosition = itemProcessor.process(item) if (assetPosition?.symbol != null) { diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/wallet/account/OrderProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/wallet/account/OrderProcessor.kt index 1053cd23b..57d55aca5 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/wallet/account/OrderProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/wallet/account/OrderProcessor.kt @@ -301,9 +301,12 @@ internal class OrderProcessor( existing: SubaccountOrder?, payload: IndexerCompositeOrderObject, ): Boolean { - val updatedAt = existing?.updatedAtMilliseconds?.let { + if (existing == null) { + return true + } + val updatedAt = existing.updatedAtMilliseconds?.let { Instant.fromEpochMilliseconds(it.toLong()) - } ?: existing?.createdAtMilliseconds?.let { + } ?: existing.createdAtMilliseconds?.let { Instant.fromEpochMilliseconds(it.toLong()) } val incomingUpdatedAt = parser.asDatetime(payload.updatedAt) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/wallet/account/PerpetualPositionProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/wallet/account/PerpetualPositionProcessor.kt index b62f7f152..8105828d9 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/wallet/account/PerpetualPositionProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/wallet/account/PerpetualPositionProcessor.kt @@ -13,6 +13,7 @@ import exchange.dydx.abacus.utils.NUM_PARENT_SUBACCOUNTS import exchange.dydx.abacus.utils.Numeric import exchange.dydx.abacus.utils.safeSet import indexer.codegen.IndexerPerpetualPositionResponseObject +import indexer.codegen.IndexerPositionSide /* "ETH-USD": { @@ -136,11 +137,19 @@ internal class PerpetualPositionProcessor( } else { sideStringKey } + val size = parser.asDouble(payload.size) + val signedSize = + if (size != null) { + if (payload.side == IndexerPositionSide.SHORT) (size.abs() * -1.0) else size + } else { + null + } + InternalPerpetualPosition( market = payload.market, status = payload.status, side = payload.side, - size = parser.asDouble(payload.size), + size = signedSize, maxSize = parser.asDouble(payload.maxSize), entryPrice = parser.asDouble(payload.entryPrice), realizedPnl = parser.asDouble(payload.realizedPnl), diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/wallet/account/SubaccountProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/wallet/account/SubaccountProcessor.kt index 465a3bc5e..3c256ade5 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/wallet/account/SubaccountProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/wallet/account/SubaccountProcessor.kt @@ -137,7 +137,7 @@ internal open class SubaccountProcessor( height = height, ) - val perpetualPositions = content["perpetualPositions"] ?: content["openPerpetualPositions"] + val perpetualPositions = content["perpetualPositions"] ?: content["openPerpetualPositions"] ?: content["positions"] val positions = parser.asTypedList(perpetualPositions) state = processPerpetualPositions( existing = state, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Wallet.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Wallet.kt index dbf6be5ea..ef96fa327 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Wallet.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Wallet.kt @@ -27,9 +27,10 @@ internal fun TradingStateMachine.receivedSubaccountSubscribed( payload: Map, height: BlockAndTime?, ): StateChanges { - this.wallet = walletProcessor.subscribedDeprecated(wallet, payload, height) if (staticTyping) { walletProcessor.processSubscribed(internalState.wallet, payload, height) + } else { + this.wallet = walletProcessor.subscribedDeprecated(wallet, payload, height) } val changes = iMutableListOf() @@ -43,17 +44,26 @@ internal fun TradingStateMachine.receivedSubaccountSubscribed( changes.add(Changes.historicalPnl) changes.add(Changes.tradingRewards) val subaccountNumber = parser.asInt(payload["subaccountNumber"]) ?: 0 - val subaccountNumbers = MarginCalculator.getChangedSubaccountNumbersDeprecated( - parser, - account, - subaccountNumber ?: 0, - parser.asMap(input?.get("trade")), - ) + val subaccountNumbers = if (staticTyping) { + MarginCalculator.getChangedSubaccountNumbers( + parser = parser, + subaccounts = internalState.wallet.account.subaccounts, + subaccountNumber = subaccountNumber, + tradeInput = internalState.input.trade, + ) + } else { + MarginCalculator.getChangedSubaccountNumbersDeprecated( + parser = parser, + account = account, + subaccountNumber = subaccountNumber ?: 0, + tradeInput = parser.asMap(input?.get("trade")), + ) + } return StateChanges( - changes, - null, - subaccountNumbers, + changes = changes, + markets = null, + subaccountNumbers = subaccountNumbers, ) } @@ -62,9 +72,10 @@ internal fun TradingStateMachine.receivedSubaccountsChanges( info: SocketInfo, height: BlockAndTime?, ): StateChanges { - this.wallet = walletProcessor.channel_dataDeprecated(wallet, payload, info, height) if (staticTyping) { walletProcessor.processChannelData(internalState.wallet, payload, info, height) + } else { + this.wallet = walletProcessor.channel_dataDeprecated(wallet, payload, info, height) } val changes = iMutableListOf() @@ -190,10 +201,11 @@ internal fun TradingStateMachine.receivedFills( val fills = parser.asList(payload["fills"]) val size = fills?.size ?: 0 return if (size > 0) { - wallet = walletProcessor.receivedFillsDeprecated(wallet, payload, subaccountNumber) if (staticTyping) { val payload = parser.asTypedObject(payload) walletProcessor.processFills(internalState.wallet, payload?.fills?.toList(), subaccountNumber) + } else { + wallet = walletProcessor.receivedFillsDeprecated(wallet, payload, subaccountNumber) } StateChanges(iListOf(Changes.fills), null, iListOf(subaccountNumber)) } else { @@ -216,10 +228,11 @@ internal fun TradingStateMachine.receivedTransfers( ): StateChanges { val size = parser.asList(payload["transfers"])?.size ?: 0 return if (size > 0) { - wallet = walletProcessor.receivedTransfersDeprecated(wallet, payload, subaccountNumber) if (staticTyping) { val payload = parser.asTypedObject(payload) walletProcessor.processTransfers(internalState.wallet, payload?.transfers?.toList(), subaccountNumber) + } else { + wallet = walletProcessor.receivedTransfersDeprecated(wallet, payload, subaccountNumber) } StateChanges(iListOf(Changes.transfers), null, iListOf(subaccountNumber)) } else { diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeBracketOrdersValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeBracketOrdersValidator.kt index 4b32b4d40..cefd3b7c6 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeBracketOrdersValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeBracketOrdersValidator.kt @@ -183,7 +183,7 @@ internal class TradeBracketOrdersValidator( tickSize: String, ): ValidationError? { when (trade.side) { - OrderSide.Buy -> { + OrderSide.Sell -> { if (triggerPrice >= price) { return triggerPriceError( errorCode = "BRACKET_ORDER_TAKE_PROFIT_BELOW_EXPECTED_PRICE", @@ -202,7 +202,7 @@ internal class TradeBracketOrdersValidator( return null } } - OrderSide.Sell -> { + OrderSide.Buy -> { if (triggerPrice <= price) { return triggerPriceError( errorCode = "BRACKET_ORDER_TAKE_PROFIT_ABOVE_EXPECTED_PRICE", diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeFieldsValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeFieldsValidator.kt index c71f44f1e..729c27159 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeFieldsValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeFieldsValidator.kt @@ -46,27 +46,27 @@ internal class TradeFieldsValidator( } } - if (trade.options.needsTriggerPrice) { - val triggerPrice = trade.price?.triggerPrice ?: 0.0 - if (triggerPrice == 0.0) { + if (trade.options.needsLimitPrice) { + val limitPrice = trade.price?.limitPrice ?: 0.0 + if (limitPrice == 0.0) { errors.add( required( - errorCode = "REQUIRED_TRIGGER_PRICE", - field = TradeInputField.triggerPrice.rawValue, - actionStringKey = "APP.TRADE.ENTER_TRIGGER_PRICE", + errorCode = "REQUIRED_LIMIT_PRICE", + field = TradeInputField.limitPrice.rawValue, + actionStringKey = "APP.TRADE.ENTER_LIMIT_PRICE", ), ) } } - if (trade.options.needsLimitPrice) { - val limitPrice = trade.price?.limitPrice ?: 0.0 - if (limitPrice == 0.0) { + if (trade.options.needsTriggerPrice) { + val triggerPrice = trade.price?.triggerPrice ?: 0.0 + if (triggerPrice == 0.0) { errors.add( required( - errorCode = "REQUIRED_LIMIT_PRICE", - field = TradeInputField.limitPrice.rawValue, - actionStringKey = "APP.TRADE.ENTER_LIMIT_PRICE", + errorCode = "REQUIRED_TRIGGER_PRICE", + field = TradeInputField.triggerPrice.rawValue, + actionStringKey = "APP.TRADE.ENTER_TRIGGER_PRICE", ), ) } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/transfer/WithdrawalCapacityValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/transfer/WithdrawalCapacityValidator.kt index bcf5b838d..5684c5d63 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/transfer/WithdrawalCapacityValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/transfer/WithdrawalCapacityValidator.kt @@ -24,6 +24,9 @@ internal class WithdrawalCapacityValidator( restricted: Boolean, environment: V4Environment? ): List? { + val configs = internalState.configs + val withdrawalCapacity = configs.withdrawalCapacity + val maxWithdrawalCapacity = withdrawalCapacity?.maxWithdrawalCapacity ?: BigDecimal.fromLong(Long.MAX_VALUE) return null } diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4AccountTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4AccountTests.kt index 261605ff4..ccbc7585e 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4AccountTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4AccountTests.kt @@ -1,7 +1,14 @@ package exchange.dydx.abacus.payload.v4 import com.ionspin.kotlin.bignum.decimal.toBigDecimal +import exchange.dydx.abacus.calculator.CalculationPeriod import exchange.dydx.abacus.output.EquityTier +import exchange.dydx.abacus.output.account.FillLiquidity +import exchange.dydx.abacus.output.account.TransferRecordType +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.responses.StateResponse import exchange.dydx.abacus.state.app.adaptors.AbUrl import exchange.dydx.abacus.state.changes.Changes @@ -21,6 +28,7 @@ import exchange.dydx.abacus.tests.extensions.parseOnChainEquityTiers import exchange.dydx.abacus.utils.JsonEncoder import exchange.dydx.abacus.utils.Parser import exchange.dydx.abacus.utils.ServerTime +import indexer.codegen.IndexerPerpetualPositionStatus import kotlinx.datetime.Clock import kotlin.test.Test import kotlin.test.assertEquals @@ -105,11 +113,29 @@ class V4AccountTests : V4BaseTests() { } private fun testSubaccountSubscribed() { - test( - { - perp.loadv4SubaccountSubscribed(mock, testWsUrl) - }, - """ + if (perp.staticTyping) { + perp.loadv4SubaccountSubscribed(mock, testWsUrl) + + val account = perp.internalState.wallet.account + assertEquals(2800.8, account.tradingRewards.total) + + val subaccount = account.subaccounts[0] + val calculated = subaccount?.calculated?.get(CalculationPeriod.current)!! + assertEquals(122034.20090508368, calculated.equity) + assertEquals(100728.9107212275, calculated.freeCollateral) + assertEquals(68257.215192, calculated.quoteBalance) + + val orders = subaccount.orders + assertEquals(2, orders?.size) + + val openPositions = subaccount.openPositions + assertEquals(2, openPositions?.size) + } else { + test( + { + perp.loadv4SubaccountSubscribed(mock, testWsUrl) + }, + """ { "wallet": { "account": { @@ -256,21 +282,33 @@ class V4AccountTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } private fun testSubaccountFillsReceived() { - test( - load = { - perp.rest( - AbUrl.fromString("$testRestUrl/v4/fills?subaccountNumber=0"), - mock.fillsChannel.v4_rest, - 0, - null, - ) - }, - expected = """ + if (perp.staticTyping) { + perp.rest( + url = AbUrl.fromString("$testRestUrl/v4/fills?subaccountNumber=0"), + payload = mock.fillsChannel.v4_rest, + subaccountNumber = 0, + height = null, + ) + + val fills = perp.internalState.wallet.account.subaccounts[0]?.fills + assertEquals(100, fills?.size) + } else { + test( + load = { + perp.rest( + AbUrl.fromString("$testRestUrl/v4/fills?subaccountNumber=0"), + mock.fillsChannel.v4_rest, + 0, + null, + ) + }, + expected = """ { "wallet": { "account": { @@ -297,30 +335,44 @@ class V4AccountTests : V4BaseTests() { } } } - """.trimIndent(), - moreVerification = { - if (perp.staticTyping) { - val fills = perp.internalState.wallet.account.subaccounts[0]?.fills - assertEquals( - 100, - fills?.size, - ) - } else { + """.trimIndent(), + moreVerification = { val fills = - parser.asList(parser.value(perp.data, "wallet.account.subaccounts.0.fills")) + parser.asList( + parser.value( + perp.data, + "wallet.account.subaccounts.0.fills", + ), + ) assertEquals( 100, fills?.size, ) - } - }, - ) + }, + ) + } - test( - { - perp.socket(testWsUrl, mock.fillsChannel.v4_subscribed, 0, null) - }, - """ + if (perp.staticTyping) { + perp.socket(testWsUrl, mock.fillsChannel.v4_subscribed, 0, null) + + val fills = perp.internalState.wallet.account.subaccounts[0]?.fills + assertEquals(100, fills?.size) + val fill = fills?.first()!! + assertEquals("dad7abeb-4c04-58d3-8dda-fd0bc0528deb", fill.id) + assertEquals(OrderSide.Buy, fill.side) + assertEquals(FillLiquidity.taker, fill.liquidity) + assertEquals(OrderType.Limit, fill.type) + assertEquals("BTC-USD", fill.marketId) + assertEquals("4f2a6f7d-a897-5c4e-986f-d48f5760102a", fill.orderId) + assertEquals(18275.31, fill.price) + assertEquals(4.41E-6, fill.size) + assertEquals(0.0, fill.fee) + } else { + test( + { + perp.socket(testWsUrl, mock.fillsChannel.v4_subscribed, 0, null) + }, + """ { "wallet": { "account": { @@ -356,23 +408,47 @@ class V4AccountTests : V4BaseTests() { } } } - """.trimIndent(), - { - }, - ) + """.trimIndent(), + { + }, + ) + } } private fun testSubaccountTransfersReceived() { - test( - { - perp.rest( - AbUrl.fromString("$testRestUrl/v4/transfers?subaccountNumber=0"), - mock.transfersMock.transfer_data, - 0, - null, - ) - }, - """ + if (perp.staticTyping) { + perp.rest( + url = AbUrl.fromString("$testRestUrl/v4/transfers?subaccountNumber=0"), + payload = mock.transfersMock.transfer_data, + subaccountNumber = 0, + height = null, + ) + + val transfers = perp.internalState.wallet.account.subaccounts[0]?.transfers + val transfer = transfers?.first()!! + assertEquals("89586775-0646-582e-9b36-4f131715644d", transfer.id) + assertEquals(TransferRecordType.TRANSFER_OUT, transfer.type) + assertEquals("USDC", transfer.asset) + assertEquals( + parser.asDatetime("2023-08-21T21:37:53.373Z")?.toEpochMilliseconds()?.toDouble(), + transfer.updatedAtMilliseconds, + ) + assertEquals(404014, transfer.updatedAtBlock) + assertEquals(419.98472, transfer.amount) + assertEquals("dydx1sxdvx2kzgdykutxfv06ka9gt0klu8wctfwskhg", transfer.fromAddress) + assertEquals("dydx1vvjr376v4hfpy5r6m3dmu4u3mu6yl6sjds3gz8", transfer.toAddress) + assertEquals("MOCKHASH1", transfer.transactionHash) + } else { + test( + { + perp.rest( + AbUrl.fromString("$testRestUrl/v4/transfers?subaccountNumber=0"), + mock.transfersMock.transfer_data, + 0, + null, + ) + }, + """ { "wallet": { "account": { @@ -402,16 +478,25 @@ class V4AccountTests : V4BaseTests() { } } } - """.trimIndent(), - { - }, - ) + """.trimIndent(), + { + }, + ) + } - test( - { - perp.socket(testWsUrl, mock.transfersMock.channel_data, 0, null) - }, - """ + if (perp.staticTyping) { + perp.socket(testWsUrl, mock.transfersMock.channel_data, 0, null) + + val transfers = perp.internalState.wallet.account.subaccounts[0]?.transfers + val transfer = transfers?.first()!! + assertEquals("A9758D092415E36F4E0D80D323BC4EE472644548392489309333CA55E963431B", transfer.id) + assertEquals("A9758D092415E36F4E0D80D323BC4EE472644548392489309333CA55E963431B", transfer.transactionHash) + } else { + test( + { + perp.socket(testWsUrl, mock.transfersMock.channel_data, 0, null) + }, + """ { "wallet": { "account": { @@ -434,16 +519,59 @@ class V4AccountTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } private fun testSubaccountFillsChannelData() { - test( - { - perp.socket(testWsUrl, mock.fillsChannel.v4_channel_data, 0, null) - }, - """ + if (perp.staticTyping) { + perp.socket(testWsUrl, mock.fillsChannel.v4_channel_data, 0, null) + + val account = perp.internalState.wallet.account + val blockReward = account.tradingRewards.blockRewards[0] + assertEquals("0.02", blockReward.tradingReward) + assertEquals("2422", blockReward.createdAtHeight) + val blockReward2 = account.tradingRewards.blockRewards[1] + assertEquals("0.01", blockReward2.tradingReward) + assertEquals("2501", blockReward2.createdAtHeight) + + val subaccount = account.subaccounts[0] + val calculated = subaccount?.calculated?.get(CalculationPeriod.current)!! + assertEquals(122034.20090508368, calculated.equity) + assertEquals(100728.9107212275, calculated.freeCollateral) + assertEquals(68257.215192, calculated.quoteBalance) + + val fills = subaccount.fills + assertEquals(101, fills?.size) + val fill = fills?.firstOrNull { it.id == "0cf41e16-036e-534d-bbaf-cf318b44b840" } + assertEquals("0cf41e16-036e-534d-bbaf-cf318b44b840", fill?.id) + assertEquals(OrderSide.Sell, fill?.side) + assertEquals(FillLiquidity.taker, fill?.liquidity) + assertEquals(OrderType.Limit, fill?.type) + assertEquals("f5d440b9-6e93-535a-a5d6-fbb74852c6d8", fill?.orderId) + assertEquals(1570.19, fill?.price) + assertEquals(0.003, fill?.size) + + val orders = subaccount.orders + assertEquals(3, orders?.size) + val order = orders?.firstOrNull { it.id == "b812bea8-29d3-5841-9549-caa072f6f8a8" } + assertEquals("b812bea8-29d3-5841-9549-caa072f6f8a8", order?.id) + assertEquals(OrderSide.Sell, order?.side) + assertEquals(OrderType.Limit, order?.type) + assertEquals(OrderTimeInForce.GTT, order?.timeInForce) + assertEquals(1255.927, order?.price) + assertEquals(1.653451, order?.size) + assertEquals(false, order?.postOnly) + assertEquals(false, order?.reduceOnly) + assertEquals(0.970818, order?.remainingSize) + assertEquals(0.682633, order?.totalFilled) + } else { + test( + { + perp.socket(testWsUrl, mock.fillsChannel.v4_channel_data, 0, null) + }, + """ { "wallet": { "account": { @@ -530,32 +658,77 @@ class V4AccountTests : V4BaseTests() { } } } - """.trimIndent(), - { - if (perp.staticTyping) { - val fills = perp.internalState.wallet.account.subaccounts[0]?.fills - assertEquals( - 101, - fills?.size, - ) - } else { + """.trimIndent(), + { val fills = - parser.asList(parser.value(perp.data, "wallet.account.subaccounts.0.fills")) + parser.asList( + parser.value( + perp.data, + "wallet.account.subaccounts.0.fills", + ), + ) assertEquals( 101, fills?.size, ) - } - }, - ) + }, + ) + } } private fun testSubaccountChanged() { - test( - { - perp.loadv4SubaccountWithOrdersAndFillsChanged(mock, testWsUrl) - }, - """ + if (perp.staticTyping) { + perp.loadv4SubaccountWithOrdersAndFillsChanged(mock, testWsUrl) + + val subaccount = perp.internalState.wallet.account.subaccounts[0]!! + + val calculated = subaccount.calculated[CalculationPeriod.current]!! + assertEquals(-161020.048352526628, calculated.equity) + assertEquals(-172483.91152975295, calculated.freeCollateral) + assertEquals(68257.215192, calculated.quoteBalance) + + val order = subaccount.orders?.firstOrNull { it.id == "b812bea8-29d3-5841-9549-caa072f6f8a8" } + assertEquals("b812bea8-29d3-5841-9549-caa072f6f8a8", order?.id) + assertEquals(OrderSide.Sell, order?.side) + assertEquals(OrderType.Limit, order?.type) + assertEquals(OrderTimeInForce.GTT, order?.timeInForce) + assertEquals(1255.927, order?.price) + assertEquals(1.653451, order?.size) + assertEquals(false, order?.postOnly) + assertEquals(false, order?.reduceOnly) + + val transfers = subaccount.transfers + assertTrue { + transfers?.any { it.id == "A9758D092415E36F4E0D80D323BC4EE472644548392489309333CA55E963431B" } == true + } + val transfer = transfers?.first { it.id == "89586775-0646-582e-9b36-4f131715644d" }!! + assertEquals("89586775-0646-582e-9b36-4f131715644d", transfer.id) + assertEquals(TransferRecordType.TRANSFER_OUT, transfer.type) + assertEquals("USDC", transfer.asset) + assertEquals(419.98472, transfer.amount) + assertEquals("dydx1sxdvx2kzgdykutxfv06ka9gt0klu8wctfwskhg", transfer.fromAddress) + assertEquals("dydx1vvjr376v4hfpy5r6m3dmu4u3mu6yl6sjds3gz8", transfer.toAddress) + assertEquals("MOCKHASH1", transfer.transactionHash) + + val fills = subaccount.fills + assertEquals(102, fills?.size) + + val ethPosition = subaccount.openPositions?.get("ETH-USD")!! + assertEquals("ETH-USD", ethPosition.market) + assertEquals(IndexerPerpetualPositionStatus.OPEN, ethPosition.status) + assertEquals(106.180627, ethPosition.maxSize) + assertEquals(0.0, ethPosition.netFunding) + assertEquals(-102.716895, ethPosition.realizedPnl) + assertEquals(-51730.736277242424, ethPosition.calculated[CalculationPeriod.current]?.unrealizedPnl) + assertEquals(parser.asDatetime("2022-12-11T17:29:39.792Z"), ethPosition.createdAt) + assertEquals(1266.094016, ethPosition.entryPrice) + assertEquals(-106.17985, ethPosition.size) + } else { + test( + { + perp.loadv4SubaccountWithOrdersAndFillsChanged(mock, testWsUrl) + }, + """ { "wallet": { "account": { @@ -695,35 +868,46 @@ class V4AccountTests : V4BaseTests() { } } } - """.trimIndent(), - { - if (perp.staticTyping) { - val fills = perp.internalState.wallet.account.subaccounts[0]?.fills - assertEquals( - 102, - fills?.size, - ) - } else { + """.trimIndent(), + { val fills = - parser.asList(parser.value(perp.data, "wallet.account.subaccounts.0.fills")) + parser.asList( + parser.value( + perp.data, + "wallet.account.subaccounts.0.fills", + ), + ) assertEquals( 102, fills?.size, ) - } - }, - ) + }, + ) + } - test( - { - perp.socket( - testWsUrl, - mock.accountsChannel.v4_best_effort_cancelled, - 0, - BlockAndTime(16940, Clock.System.now()), - ) - }, - """ + if (perp.staticTyping) { + perp.socket( + url = testWsUrl, + jsonString = mock.accountsChannel.v4_best_effort_cancelled, + subaccountNumber = 0, + height = BlockAndTime(16940, Clock.System.now()), + ) + + val order = perp.internalState.wallet.account.subaccounts[0]?.orders?.firstOrNull { + it.id == "80133551-6d61-573b-9788-c1488e11027a" + } + assertEquals(OrderStatus.Pending, order?.status) + } else { + test( + { + perp.socket( + testWsUrl, + mock.accountsChannel.v4_best_effort_cancelled, + 0, + BlockAndTime(16940, Clock.System.now()), + ) + }, + """ { "wallet": { "account": { @@ -740,19 +924,33 @@ class V4AccountTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } - test( - { - perp.socket( - testWsUrl, - mock.accountsChannel.v4_best_effort_cancelled, - 0, - BlockAndTime(16960, Clock.System.now()), - ) - }, - """ + if (perp.staticTyping) { + perp.socket( + url = testWsUrl, + jsonString = mock.accountsChannel.v4_best_effort_cancelled, + subaccountNumber = 0, + height = BlockAndTime(16960, Clock.System.now()), + ) + + val order = perp.internalState.wallet.account.subaccounts[0]?.orders?.firstOrNull { + it.id == "80133551-6d61-573b-9788-c1488e11027a" + } + assertEquals(OrderStatus.Canceled, order?.status) + } else { + test( + { + perp.socket( + testWsUrl, + mock.accountsChannel.v4_best_effort_cancelled, + 0, + BlockAndTime(16960, Clock.System.now()), + ) + }, + """ { "wallet": { "account": { @@ -769,14 +967,23 @@ class V4AccountTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } - test( - { - perp.updateHeight(BlockAndTime(16960, Clock.System.now())) - }, - """ + if (perp.staticTyping) { + perp.updateHeight(BlockAndTime(16960, Clock.System.now())) + + val order = perp.internalState.wallet.account.subaccounts[0]?.orders?.firstOrNull { + it.id == "80133551-6d61-573b-9788-c1488e11027a" + } + assertEquals(OrderStatus.Canceled, order?.status) + } else { + test( + { + perp.updateHeight(BlockAndTime(16960, Clock.System.now())) + }, + """ { "wallet": { "account": { @@ -793,21 +1000,40 @@ class V4AccountTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } private fun testBatchedSubaccountChanged() { - test( - { - perp.socket( - testWsUrl, - mock.accountsChannel.v4_batched, - 0, - BlockAndTime(16960, Clock.System.now()), - ) - }, - """ + if (perp.staticTyping) { + perp.socket( + url = testWsUrl, + jsonString = mock.accountsChannel.v4_batched, + subaccountNumber = 0, + height = BlockAndTime(16960, Clock.System.now()), + ) + + val subaccount = perp.internalState.wallet.account.subaccounts[0]!! + val calculated = subaccount.calculated[CalculationPeriod.current]!! + assertEquals(-41124.184464506594, calculated.equity) + + val order = subaccount.orders?.firstOrNull { it.id == "1118c548-1715-5a72-9c41-f4388518c6e2" } + assertEquals(OrderStatus.PartiallyFilled, order?.status) + + val fills = subaccount.fills + assertEquals(112, fills?.size) + } else { + test( + { + perp.socket( + testWsUrl, + mock.accountsChannel.v4_batched, + 0, + BlockAndTime(16960, Clock.System.now()), + ) + }, + """ { "wallet": { "account": { @@ -833,35 +1059,77 @@ class V4AccountTests : V4BaseTests() { } } } - """.trimIndent(), - { - if (perp.staticTyping) { - val fills = perp.internalState.wallet.account.subaccounts[0]?.fills - assertEquals( - 112, - fills?.size, - ) - } else { + """.trimIndent(), + { val fills = - parser.asList(parser.value(perp.data, "wallet.account.subaccounts.0.fills")) + parser.asList( + parser.value( + perp.data, + "wallet.account.subaccounts.0.fills", + ), + ) assertEquals( 112, fills?.size, ) - } - }, - ) + }, + ) + } - test( - load = { - perp.socket( - testWsUrl, - mock.accountsChannel.v4_position_closed, - 0, - BlockAndTime(16961, Clock.System.now()), + if (perp.staticTyping) { + perp.socket( + url = testWsUrl, + jsonString = mock.accountsChannel.v4_position_closed, + subaccountNumber = 0, + height = BlockAndTime(16961, Clock.System.now()), + ) + + val subaccount = perp.internalState.wallet.account.subaccounts[0]!! + val calculated = subaccount.calculated[CalculationPeriod.current]!! + assertEquals(-41281.9808525066, calculated.equity) + val btcPosition = subaccount.openPositions?.get("BTC-USD")!! + val positionCalculated = btcPosition.calculated[CalculationPeriod.current]!! + assertEquals(-1.792239322, btcPosition.size) + + val ioImplementations = testIOImplementations() + val localizer = testLocalizer(ioImplementations) + val uiImplementations = testUIImplementations(localizer) + val notificationsProvider = + NotificationsProvider( + stateMachine = perp, + uiImplementations = uiImplementations, + environment = mock.v4Environment, + parser = Parser(), + jsonEncoder = JsonEncoder(), ) - }, - expected = """ + val notifications = notificationsProvider.buildNotifications(0) + assertEquals( + 6, + notifications.size, + ) + val order = notifications["order:1118c548-1715-5a72-9c41-f4388518c6e2"] + assertNotNull(order) + assertEquals( + "NOTIFICATIONS.ORDER_PARTIAL_FILL.TITLE", + order.title, + ) + val position = notifications["position:ETH-USD"] + assertNotNull(position) + assertEquals( + "NOTIFICATIONS.POSITION_CLOSED.TITLE", + position.title, + ) + } else { + test( + load = { + perp.socket( + testWsUrl, + mock.accountsChannel.v4_position_closed, + 0, + BlockAndTime(16961, Clock.System.now()), + ) + }, + expected = """ { "wallet": { "account": { @@ -882,38 +1150,39 @@ class V4AccountTests : V4BaseTests() { } } } - """.trimIndent(), - moreVerification = { - val ioImplementations = testIOImplementations() - val localizer = testLocalizer(ioImplementations) - val uiImplementations = testUIImplementations(localizer) - val notificationsProvider = - NotificationsProvider( - perp, - uiImplementations, - environment = mock.v4Environment, - Parser(), - JsonEncoder(), + """.trimIndent(), + moreVerification = { + 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( + 6, + notifications.size, ) - val notifications = notificationsProvider.buildNotifications(0) - assertEquals( - 6, - notifications.size, - ) - val order = notifications["order:1118c548-1715-5a72-9c41-f4388518c6e2"] - assertNotNull(order) - assertEquals( - "NOTIFICATIONS.ORDER_PARTIAL_FILL.TITLE", - order.title, - ) - val position = notifications["position:ETH-USD"] - assertNotNull(position) - assertEquals( - "NOTIFICATIONS.POSITION_CLOSED.TITLE", - position.title, - ) - }, - ) + val order = notifications["order:1118c548-1715-5a72-9c41-f4388518c6e2"] + assertNotNull(order) + assertEquals( + "NOTIFICATIONS.ORDER_PARTIAL_FILL.TITLE", + order.title, + ) + val position = notifications["position:ETH-USD"] + assertNotNull(position) + assertEquals( + "NOTIFICATIONS.POSITION_CLOSED.TITLE", + position.title, + ) + }, + ) + } } private fun testPartiallyFilledAndCanceledOrders() { diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4ClosePositionTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4ClosePositionTests.kt index 262e586b8..1818ecff6 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4ClosePositionTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4ClosePositionTests.kt @@ -1,11 +1,16 @@ package exchange.dydx.abacus.payload.v4 +import exchange.dydx.abacus.calculator.CalculationPeriod +import exchange.dydx.abacus.output.input.InputType +import exchange.dydx.abacus.output.input.OrderSide +import exchange.dydx.abacus.output.input.OrderType import exchange.dydx.abacus.state.manager.StatsigConfig import exchange.dydx.abacus.state.model.ClosePositionInputField import exchange.dydx.abacus.state.model.closePosition import exchange.dydx.abacus.tests.extensions.log import exchange.dydx.abacus.utils.ServerTime import kotlin.test.Test +import kotlin.test.assertEquals class V4ClosePositionTests : V4BaseTests() { @Test @@ -23,6 +28,7 @@ class V4ClosePositionTests : V4BaseTests() { } override fun setup() { + perp.internalState.wallet.walletAddress = "0x1234567890" loadMarkets() loadMarketsConfigurations() // do not load account @@ -35,11 +41,23 @@ class V4ClosePositionTests : V4BaseTests() { /* Initial setup */ - test( - { - perp.closePosition("ETH-USD", ClosePositionInputField.market, 0) - }, - """ + if (perp.staticTyping) { + perp.closePosition("ETH-USD", ClosePositionInputField.market, 0) + + assertEquals(perp.internalState.input.currentType, InputType.CLOSE_POSITION) + val closePosition = perp.internalState.input.closePosition + assertEquals(closePosition.type, OrderType.Market) + assertEquals(closePosition.side, OrderSide.Sell) + assertEquals(closePosition.sizePercent, 1.0) + assertEquals(closePosition.size?.size, 10.771) + assertEquals(closePosition.size?.input, "size.percent") + assertEquals(closePosition.reduceOnly, true) + } else { + test( + { + perp.closePosition("ETH-USD", ClosePositionInputField.market, 0) + }, + """ { "input": { "current": "closePosition", @@ -55,14 +73,43 @@ class V4ClosePositionTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } - test( - { - perp.closePosition("0.25", ClosePositionInputField.percent, 0) - }, - """ + if (perp.staticTyping) { + perp.closePosition("0.25", ClosePositionInputField.percent, 0) + + val closePosition = perp.internalState.input.closePosition + assertEquals(closePosition.sizePercent, 0.25) + assertEquals(closePosition.size?.size, 2.692) + assertEquals(closePosition.size?.input, "size.percent") + assertEquals(closePosition.size?.usdcSize, 4453.3756) + val summary = closePosition.summary!! + assertEquals(summary.price, 1654.3) + assertEquals(summary.size, 2.692) + assertEquals(summary.usdcSize, 4453.3756) + assertEquals(summary.total, 4453.3756) + + val subaccount = perp.internalState.wallet.account.subaccounts[0]!! + assertEquals( + subaccount.calculated[CalculationPeriod.current]?.quoteBalance, + 99872.368956, + ) + assertEquals( + subaccount.calculated[CalculationPeriod.post]?.quoteBalance, + 104592.23425040001, + ) + + val position = subaccount.openPositions?.get("ETH-USD")!! + assertEquals(position.calculated[CalculationPeriod.current]?.size, 10.771577) + assertEquals(position.calculated[CalculationPeriod.post]?.size, 8.079577) + } else { + test( + { + perp.closePosition("0.25", ClosePositionInputField.percent, 0) + }, + """ { "input": { "current": "closePosition", @@ -105,14 +152,43 @@ class V4ClosePositionTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } - test( - { - perp.closePosition("9", ClosePositionInputField.size, 0) - }, - """ + if (perp.staticTyping) { + perp.closePosition("9", ClosePositionInputField.size, 0) + + val closePosition = perp.internalState.input.closePosition + assertEquals(closePosition.sizePercent, null) + assertEquals(closePosition.size?.size, 9.0) + assertEquals(closePosition.size?.input, "size.size") + assertEquals(closePosition.size?.usdcSize, 14888.699999999999) + val summary = closePosition.summary!! + assertEquals(summary.price, 1654.3) + assertEquals(summary.size, 9.0) + assertEquals(summary.usdcSize, 14888.699999999999) + assertEquals(summary.total, 14888.699999999999) + + val subaccount = perp.internalState.wallet.account.subaccounts[0]!! + assertEquals( + subaccount.calculated[CalculationPeriod.current]?.quoteBalance, + 99872.368956, + ) + assertEquals( + subaccount.calculated[CalculationPeriod.post]?.quoteBalance, + 115652.007756, + ) + + val position = subaccount.openPositions?.get("ETH-USD")!! + assertEquals(position.calculated[CalculationPeriod.current]?.size, 10.771577) + assertEquals(position.calculated[CalculationPeriod.post]?.size, 1.7715770000000006) + } else { + test( + { + perp.closePosition("9", ClosePositionInputField.size, 0) + }, + """ { "input": { "current": "closePosition", @@ -155,8 +231,9 @@ class V4ClosePositionTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } private fun testCloseShortPositionInput() { @@ -169,14 +246,28 @@ class V4ClosePositionTests : V4BaseTests() { } """.trimIndent(), ) + /* Initial setup */ - test( - { - perp.closePosition("ETH-USD", ClosePositionInputField.market, 0) - }, - """ + + if (perp.staticTyping) { + perp.closePosition("ETH-USD", ClosePositionInputField.market, 0) + + assertEquals(perp.internalState.input.currentType, InputType.CLOSE_POSITION) + val closePosition = perp.internalState.input.closePosition + assertEquals(closePosition.type, OrderType.Market) + assertEquals(closePosition.side, OrderSide.Buy) + assertEquals(closePosition.sizePercent, 1.0) + assertEquals(closePosition.size?.size, 106.179) + assertEquals(closePosition.size?.input, "size.percent") + assertEquals(closePosition.reduceOnly, true) + } else { + test( + { + perp.closePosition("ETH-USD", ClosePositionInputField.market, 0) + }, + """ { "input": { "current": "closePosition", @@ -192,14 +283,37 @@ class V4ClosePositionTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } - test( - { - perp.closePosition("0.25", ClosePositionInputField.percent, 0) - }, - """ + if (perp.staticTyping) { + perp.closePosition("0.25", ClosePositionInputField.percent, 0) + + val closePosition = perp.internalState.input.closePosition + assertEquals(closePosition.sizePercent, 0.25) + assertEquals(closePosition.size?.size, 2.6544E+1) + assertEquals(closePosition.size?.input, "size.percent") + + val subaccount = perp.internalState.wallet.account.subaccounts[0]!! + assertEquals( + subaccount.calculated[CalculationPeriod.current]?.quoteBalance, + 68257.215192, + ) + assertEquals( + subaccount.calculated[CalculationPeriod.post]?.quoteBalance, + 24308.314392, + ) + + val position = subaccount.openPositions?.get("ETH-USD")!! + assertEquals(position.calculated[CalculationPeriod.current]?.size, -106.17985) + assertEquals(position.calculated[CalculationPeriod.post]?.size, -79.63585) + } else { + test( + { + perp.closePosition("0.25", ClosePositionInputField.percent, 0) + }, + """ { "input": { "current": "closePosition", @@ -235,14 +349,44 @@ class V4ClosePositionTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } - test( - { - perp.closePosition("15", ClosePositionInputField.size, 0) - }, - """ + if (perp.staticTyping) { + perp.closePosition("15", ClosePositionInputField.size, 0) + + val closePosition = perp.internalState.input.closePosition + assertEquals(closePosition.sizePercent, null) + assertEquals(closePosition.size?.size, 15.0) + assertEquals(closePosition.size?.input, "size.size") + assertEquals(closePosition.size?.usdcSize, 24835.5) + + val summary = closePosition.summary!! + assertEquals(summary.price, 1655.7) + assertEquals(summary.size, 15.0) + assertEquals(summary.usdcSize, 24835.5) + assertEquals(summary.total, -24835.5) + + val subaccount = perp.internalState.wallet.account.subaccounts[0]!! + assertEquals( + subaccount.calculated[CalculationPeriod.current]?.quoteBalance, + 68257.215192, + ) + assertEquals( + subaccount.calculated[CalculationPeriod.post]?.quoteBalance, + 43421.715192, + ) + + val position = subaccount.openPositions?.get("ETH-USD")!! + assertEquals(position.calculated[CalculationPeriod.current]?.size, -106.17985) + assertEquals(position.calculated[CalculationPeriod.post]?.size, -91.17985) + } else { + test( + { + perp.closePosition("15", ClosePositionInputField.size, 0) + }, + """ { "input": { "current": "closePosition", @@ -285,8 +429,9 @@ class V4ClosePositionTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } private fun testLimitClosePositionInput() { @@ -300,14 +445,28 @@ class V4ClosePositionTests : V4BaseTests() { } """.trimIndent(), ) + /* Initial setup */ - test( - { - perp.closePosition("ETH-USD", ClosePositionInputField.market, 0) - }, - """ + + if (perp.staticTyping) { + perp.closePosition("ETH-USD", ClosePositionInputField.market, 0) + + assertEquals(perp.internalState.input.currentType, InputType.CLOSE_POSITION) + val closePosition = perp.internalState.input.closePosition + assertEquals(closePosition.type, OrderType.Market) + assertEquals(closePosition.side, OrderSide.Buy) + assertEquals(closePosition.sizePercent, 1.0) + assertEquals(closePosition.size?.size, 106.179) + assertEquals(closePosition.size?.input, "size.percent") + assertEquals(closePosition.reduceOnly, true) + } else { + test( + { + perp.closePosition("ETH-USD", ClosePositionInputField.market, 0) + }, + """ { "input": { "current": "closePosition", @@ -323,14 +482,27 @@ class V4ClosePositionTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } - test( - { - perp.closePosition("true", ClosePositionInputField.useLimit, 0) - }, - """ + if (perp.staticTyping) { + perp.closePosition("true", ClosePositionInputField.useLimit, 0) + + val closePosition = perp.internalState.input.closePosition + assertEquals(closePosition.type, OrderType.Limit) + assertEquals(closePosition.side, OrderSide.Buy) + assertEquals(closePosition.sizePercent, 1.0) + assertEquals(closePosition.size?.size, 106.179) + assertEquals(closePosition.size?.input, "size.percent") + assertEquals(closePosition.price?.limitPrice, 1655.0) + assertEquals(closePosition.reduceOnly, true) + } else { + test( + { + perp.closePosition("true", ClosePositionInputField.useLimit, 0) + }, + """ { "input": { "current": "closePosition", @@ -343,20 +515,27 @@ class V4ClosePositionTests : V4BaseTests() { "size": 106.179 }, "price": { - "limitPrice": 2000 + "limitPrice": 1655 }, "reduceOnly": true } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } - test( - { - perp.closePosition("2500", ClosePositionInputField.limitPrice, 0) - }, - """ + if (perp.staticTyping) { + perp.closePosition("2500", ClosePositionInputField.limitPrice, 0) + + val closePosition = perp.internalState.input.closePosition + assertEquals(closePosition.price?.limitPrice, 2500.0) + } else { + test( + { + perp.closePosition("2500", ClosePositionInputField.limitPrice, 0) + }, + """ { "input": { "current": "closePosition", @@ -375,14 +554,27 @@ class V4ClosePositionTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } - test( - { - perp.closePosition("false", ClosePositionInputField.useLimit, 0) - }, - """ + if (perp.staticTyping) { + perp.closePosition("false", ClosePositionInputField.useLimit, 0) + + val closePosition = perp.internalState.input.closePosition + assertEquals(closePosition.type, OrderType.Market) + assertEquals(closePosition.side, OrderSide.Buy) + assertEquals(closePosition.sizePercent, 1.0) + assertEquals(closePosition.size?.size, 106.179) + assertEquals(closePosition.size?.input, "size.percent") + assertEquals(closePosition.price?.limitPrice, 2500.0) + assertEquals(closePosition.reduceOnly, true) + } else { + test( + { + perp.closePosition("false", ClosePositionInputField.useLimit, 0) + }, + """ { "input": { "current": "closePosition", @@ -401,7 +593,8 @@ class V4ClosePositionTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } } diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4NoAccountTradeInputTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4NoAccountTradeInputTests.kt index f9e9641f1..715892cc5 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4NoAccountTradeInputTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4NoAccountTradeInputTests.kt @@ -1,5 +1,6 @@ package exchange.dydx.abacus.payload.v4 +import exchange.dydx.abacus.output.input.ErrorType import exchange.dydx.abacus.state.model.TradeInputField import exchange.dydx.abacus.state.model.trade import exchange.dydx.abacus.state.model.tradeInMarket @@ -67,11 +68,23 @@ class V4NoAccountTradeInputTests : V4BaseTests() { perp.trade("1500", TradeInputField.limitPrice, 0) }, null) - test( - { - perp.trade("1", TradeInputField.limitPrice, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("1", TradeInputField.limitPrice, 0) + + val errors = perp.internalState.input.errors + val error = errors?.get(0) + assertEquals(error?.type, ErrorType.error) + assertEquals(error?.code, "REQUIRED_WALLET") + assertEquals(error?.action?.rawValue, "/onboard") + assertEquals(error?.resources?.title?.stringKey, "ERRORS.TRADE_BOX_TITLE.CONNECT_WALLET_TO_TRADE") + assertEquals(error?.resources?.text?.stringKey, "ERRORS.TRADE_BOX.CONNECT_WALLET_TO_TRADE") + assertEquals(error?.resources?.action?.stringKey, "ERRORS.TRADE_BOX_TITLE.CONNECT_WALLET_TO_TRADE") + } else { + test( + { + perp.trade("1", TradeInputField.limitPrice, 0) + }, + """ { "input": { "errors": [ @@ -94,7 +107,8 @@ class V4NoAccountTradeInputTests : V4BaseTests() { ] } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } } diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4SubaccountTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4SubaccountTests.kt index e866bac1d..b9d38cf23 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4SubaccountTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4SubaccountTests.kt @@ -1,9 +1,12 @@ package exchange.dydx.abacus.payload.v4 +import exchange.dydx.abacus.calculator.CalculationPeriod import exchange.dydx.abacus.responses.StateResponse import exchange.dydx.abacus.state.app.adaptors.AbUrl import exchange.dydx.abacus.utils.ServerTime +import indexer.codegen.IndexerPerpetualPositionStatus import kotlin.test.Test +import kotlin.test.assertEquals class V4SubaccountTests : V4BaseTests() { @Test @@ -69,16 +72,32 @@ class V4SubaccountTests : V4BaseTests() { } private fun testSubaccountSubscribed() { - test( - { - perp.socket( - testWsUrl, - mock.batchedSubaccountsChannel.subscribed, - 0, - null, - ) - }, - """ + if (perp.staticTyping) { + perp.socket( + url = testWsUrl, + jsonString = mock.batchedSubaccountsChannel.subscribed, + subaccountNumber = 0, + height = null, + ) + + val account = perp.internalState.wallet.account + assertEquals(account.tradingRewards.total, 36059.40741180069) + val subaccount = account.subaccounts[0]!! + val calculated = subaccount.calculated[CalculationPeriod.current] + assertEquals(calculated?.equity, 623694.7306634021) + assertEquals(calculated?.freeCollateral, 485562.68948613136) + assertEquals(calculated?.quoteBalance, 1625586.093553) + } else { + test( + { + perp.socket( + testWsUrl, + mock.batchedSubaccountsChannel.subscribed, + 0, + null, + ) + }, + """ { "wallet": { "account": { @@ -105,21 +124,38 @@ class V4SubaccountTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } private fun testSubaccountChanged1() { - test( - { - perp.socket( - testWsUrl, - mock.batchedSubaccountsChannel.channel_batch_data_1, - 0, - null, - ) - }, - """ + if (perp.staticTyping) { + perp.socket( + url = testWsUrl, + jsonString = mock.batchedSubaccountsChannel.channel_batch_data_1, + subaccountNumber = 0, + height = null, + ) + + val account = perp.internalState.wallet.account + assertEquals(account.tradingRewards.total, 36059.40741180069) + val subaccount = account.subaccounts[0]!! + val calculated = subaccount.calculated[CalculationPeriod.current] + assertEquals(calculated?.equity, 623694.7306634021) + assertEquals(calculated?.freeCollateral, 485562.68948613136) + assertEquals(calculated?.quoteBalance, 1625586.093553) + } else { + test( + { + perp.socket( + testWsUrl, + mock.batchedSubaccountsChannel.channel_batch_data_1, + 0, + null, + ) + }, + """ { "wallet":{ "account":{ @@ -2068,21 +2104,48 @@ class V4SubaccountTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } private fun testSubaccountChangedWithFills1() { - test( - { - perp.socket( - testWsUrl, - mock.batchedSubaccountsChannel.channel_batch_data_order_filled_1, - 0, - null, - ) - }, - """ + if (perp.staticTyping) { + perp.socket( + url = testWsUrl, + jsonString = mock.batchedSubaccountsChannel.channel_batch_data_order_filled_1, + subaccountNumber = 0, + height = null, + ) + + val account = perp.internalState.wallet.account + assertEquals(account.tradingRewards.total, 36059.40741180069) + val subaccount = account.subaccounts[0]!! + val calculated = subaccount.calculated[CalculationPeriod.current] + assertEquals(calculated?.quoteBalance, 1599696.370275) + val openPositions = subaccount.openPositions + assertEquals(openPositions?.size, 42) + val aptPosition = openPositions?.get("APT-USD") + assertEquals(aptPosition?.status, IndexerPerpetualPositionStatus.OPEN) + assertEquals(aptPosition?.calculated?.get(CalculationPeriod.current)?.size, -2776.0) + assertEquals(aptPosition?.entryPrice, 9.129870549819497) + assertEquals(aptPosition?.exitPrice, 9.132496184181717) + assertEquals(aptPosition?.netFunding, 4.708773) + assertEquals(aptPosition?.maxSize, -42.0) + + val fills = subaccount.fills + assertEquals(fills?.size, 2) + } else { + test( + { + perp.socket( + testWsUrl, + mock.batchedSubaccountsChannel.channel_batch_data_order_filled_1, + 0, + null, + ) + }, + """ { "wallet": { "account": { @@ -2148,7 +2211,8 @@ class V4SubaccountTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } } diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4TradeInputTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4TradeInputTests.kt index 80ca829d2..5e2306cf1 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4TradeInputTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4TradeInputTests.kt @@ -1,11 +1,20 @@ package exchange.dydx.abacus.payload.v4 +import exchange.dydx.abacus.calculator.CalculationPeriod +import exchange.dydx.abacus.output.input.ErrorType +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.ReceiptLine import exchange.dydx.abacus.responses.StateResponse import exchange.dydx.abacus.state.model.TradeInputField import exchange.dydx.abacus.state.model.trade import exchange.dydx.abacus.state.model.tradeInMarket import exchange.dydx.abacus.tests.extensions.loadv4Accounts +import kollections.iListOf import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull open class V4TradeInputTests : V4BaseTests() { @Test @@ -34,6 +43,9 @@ open class V4TradeInputTests : V4BaseTests() { override fun setup() { super.setup() loadOrderbook() + + // connect wallet + perp.internalState.wallet.walletAddress = "0x1234567890" } override fun loadMarkets(): StateResponse { @@ -57,15 +69,16 @@ open class V4TradeInputTests : V4BaseTests() { } private fun testLimitTradeInputOnce() { - test({ - perp.trade("CROSS", TradeInputField.marginMode, 0) - }, null) - - test( - { - perp.tradeInMarket("ETH-USD", 0) - }, - """ + if (perp.staticTyping) { + perp.tradeInMarket("ETH-USD", 0) + val trade = perp.internalState.input.trade + assertEquals(trade.marketId, "ETH-USD") + } else { + test( + { + perp.tradeInMarket("ETH-USD", 0) + }, + """ { "input": { "trade": { @@ -73,8 +86,9 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } test({ perp.trade("LIMIT", TradeInputField.type, 0) @@ -92,11 +106,22 @@ open class V4TradeInputTests : V4BaseTests() { perp.trade("1500", TradeInputField.limitPrice, 0) }, null) - test( - { - perp.trade("1", TradeInputField.limitPrice, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("1", TradeInputField.limitPrice, 0) + + val account = perp.internalState.wallet.account + val subaccount = account.subaccounts[0] + val openPosition = subaccount?.openPositions?.get("ETH-USD") + assertEquals(subaccount?.calculated?.get(CalculationPeriod.current)?.equity, 100000.0) + assertEquals(subaccount?.calculated?.get(CalculationPeriod.post)?.equity, 100299.8) + assertEquals(openPosition?.calculated?.get(CalculationPeriod.current)?.valueTotal, 0.0) + assertEquals(openPosition?.calculated?.get(CalculationPeriod.post)?.valueTotal, 300.0) + } else { + test( + { + perp.trade("1", TradeInputField.limitPrice, 0) + }, + """ { "wallet": { "account": { @@ -104,7 +129,7 @@ open class V4TradeInputTests : V4BaseTests() { "0": { "equity": { "current": 100000.0, - "postOrder": 100000.0 + "postOrder": 100299.8 }, "openPositions": { "ETH-USD": { @@ -119,14 +144,26 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } - test( - { - perp.trade("10000", TradeInputField.limitPrice, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("10000", TradeInputField.limitPrice, 0) + + val account = perp.internalState.wallet.account + val subaccount = account.subaccounts[0] + val openPosition = subaccount?.openPositions?.get("ETH-USD") + assertEquals(subaccount?.calculated?.get(CalculationPeriod.current)?.equity, 100000.0) + assertEquals(subaccount?.calculated?.get(CalculationPeriod.post)?.equity, 100000.0) + assertEquals(openPosition?.calculated?.get(CalculationPeriod.current)?.valueTotal, 0.0) + assertEquals(openPosition?.calculated?.get(CalculationPeriod.post)?.valueTotal, 300.0) + } else { + test( + { + perp.trade("10000", TradeInputField.limitPrice, 0) + }, + """ { "wallet": { "account": { @@ -149,14 +186,37 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) - - test( - { - perp.trade("SELL", TradeInputField.side, 0) - }, - """ + """.trimIndent(), + ) + } + + if (perp.staticTyping) { + perp.trade("SELL", TradeInputField.side, 0) + + val account = perp.internalState.wallet.account + val subaccount = account.subaccounts[0] + val openPosition = subaccount?.openPositions?.get("ETH-USD") + assertEquals(subaccount?.calculated?.get(CalculationPeriod.current)?.equity, 100000.0) + assertEquals(subaccount?.calculated?.get(CalculationPeriod.post)?.equity, 101700.0) + assertEquals(openPosition?.calculated?.get(CalculationPeriod.current)?.valueTotal, 0.0) + assertEquals(openPosition?.calculated?.get(CalculationPeriod.post)?.valueTotal, -300.0) + + val trade = perp.internalState.input.trade + assertEquals(trade.side, OrderSide.Sell) + assertEquals(trade.marketId, "ETH-USD") + assertEquals(trade.size?.size, 0.2) + assertEquals(trade.price?.limitPrice, 10000.0) + + assertEquals( + perp.internalState.input.receiptLines, + listOf(ReceiptLine.LiquidationPrice, ReceiptLine.PositionMargin, ReceiptLine.PositionLeverage, ReceiptLine.Fee, ReceiptLine.Reward), + ) + } else { + test( + { + perp.trade("SELL", TradeInputField.side, 0) + }, + """ { "wallet": { "account": { @@ -164,7 +224,7 @@ open class V4TradeInputTests : V4BaseTests() { "0": { "equity": { "current": 100000.0, - "postOrder": 100000.0 + "postOrder": 101700.0 }, "openPositions": { "ETH-USD": { @@ -198,14 +258,26 @@ open class V4TradeInputTests : V4BaseTests() { ] } } - """.trimIndent(), - ) - - test( - { - perp.trade("1", TradeInputField.limitPrice, 0) - }, - """ + """.trimIndent(), + ) + } + + if (perp.staticTyping) { + perp.trade("1", TradeInputField.limitPrice, 0) + + val account = perp.internalState.wallet.account + val subaccount = account.subaccounts[0] + val openPosition = subaccount?.openPositions?.get("ETH-USD") + assertEquals(subaccount?.calculated?.get(CalculationPeriod.current)?.equity, 100000.0) + assertEquals(subaccount?.calculated?.get(CalculationPeriod.post)?.equity, 100000.0) + assertEquals(openPosition?.calculated?.get(CalculationPeriod.current)?.valueTotal, 0.0) + assertEquals(openPosition?.calculated?.get(CalculationPeriod.post)?.valueTotal, -300.0) + } else { + test( + { + perp.trade("1", TradeInputField.limitPrice, 0) + }, + """ { "wallet": { "account": { @@ -228,18 +300,28 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } test({ perp.trade("1500", TradeInputField.limitPrice, 0) }, null) - test( - { - perp.tradeInMarket("BTC-USD", 0) - }, - """ + if (perp.staticTyping) { + perp.tradeInMarket("BTC-USD", 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.side, OrderSide.Sell) + assertEquals(trade.marketId, "BTC-USD") + assertEquals(trade.size, null) + assertEquals(trade.price, null) + } else { + test( + { + perp.tradeInMarket("BTC-USD", 0) + }, + """ { "input": { "current": "trade", @@ -250,8 +332,9 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } test({ perp.trade("10000", TradeInputField.limitPrice, 0) @@ -261,11 +344,20 @@ open class V4TradeInputTests : V4BaseTests() { perp.trade("0.1", TradeInputField.size, 0) }, null) - test( - { - perp.trade("190", TradeInputField.goodTilDuration, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("190", TradeInputField.goodTilDuration, 0) + + val errors = perp.internalState.input.errors + assertNotNull(errors) + val error = errors[0] + assertEquals(error.type, ErrorType.error) + assertEquals(error.code, "INVALID_GOOD_TIL") + } else { + test( + { + perp.trade("190", TradeInputField.goodTilDuration, 0) + }, + """ { "input": { "errors": [ @@ -290,8 +382,9 @@ open class V4TradeInputTests : V4BaseTests() { ] } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } private fun testMarketTradeInputOnce() { @@ -313,11 +406,20 @@ open class V4TradeInputTests : V4BaseTests() { """.trimIndent(), ) - test( - { - perp.trade("1", TradeInputField.size, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("1", TradeInputField.size, 0) + val errors = perp.internalState.input.errors + assertNotNull(errors) + val error = errors[0] + assertEquals(error.type, ErrorType.error) + assertEquals(error.code, "MARKET_ORDER_NOT_ENOUGH_LIQUIDITY") + assertEquals(error.fields, iListOf("size.size")) + } else { + test( + { + perp.trade("1", TradeInputField.size, 0) + }, + """ { "input": { "errors": [ @@ -342,8 +444,9 @@ open class V4TradeInputTests : V4BaseTests() { ] } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } test( { @@ -375,11 +478,25 @@ open class V4TradeInputTests : V4BaseTests() { """.trimIndent(), ) - test( - { - perp.trade("0.5", TradeInputField.usdcSize, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("0.5", TradeInputField.usdcSize, 0) + + val size = perp.internalState.input.trade.size + assertNotNull(size) + assertEquals(size.usdcSize, 0.5) + assertEquals(size.size, 0.0) + + val errors = perp.internalState.input.errors + assertNotNull(errors) + val error = errors[0] + assertEquals(error.type, ErrorType.error) + assertEquals(error.code, "ORDER_SIZE_BELOW_MIN_SIZE") + } else { + test( + { + perp.trade("0.5", TradeInputField.usdcSize, 0) + }, + """ { "input": { "trade": { @@ -396,14 +513,23 @@ open class V4TradeInputTests : V4BaseTests() { ] } } - """.trimIndent(), - ) - - test( - { - perp.trade(null, TradeInputField.usdcSize, 0) - }, - """ + """.trimIndent(), + ) + } + + if (perp.staticTyping) { + perp.trade(null, TradeInputField.usdcSize, 0) + val errors = perp.internalState.input.errors + assertNotNull(errors) + val error = errors[0] + assertEquals(error.type, ErrorType.required) + assertEquals(error.code, "REQUIRED_SIZE") + } else { + test( + { + perp.trade(null, TradeInputField.usdcSize, 0) + }, + """ { "input": { "errors": [ @@ -414,8 +540,9 @@ open class V4TradeInputTests : V4BaseTests() { ] } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } private fun testIsolatedLimitTradeInputOnce() { @@ -571,14 +698,21 @@ open class V4TradeInputTests : V4BaseTests() { } private fun testUpdates() { - test({ - perp.trade("CROSS", TradeInputField.marginMode, 0) - }, null) - test( - { - perp.socket(testWsUrl, mock.accountsChannel.v4_subaccounts_update_1, 0, null) - }, - """ + if (perp.staticTyping) { + perp.socket(testWsUrl, mock.accountsChannel.v4_subaccounts_update_1, 0, null) + val account = perp.internalState.wallet.account + val subaccount = account.subaccounts[0] + val calculated = subaccount?.calculated + assertEquals(calculated?.get(CalculationPeriod.current)?.equity, 4185.625704) + assertEquals(calculated?.get(CalculationPeriod.current)?.quoteBalance, 7250.506704) + val position = subaccount?.openPositions?.get("ETH-USD") + assertEquals(position?.calculated?.get(CalculationPeriod.current)?.size, -2.043254) + } else { + test( + { + perp.socket(testWsUrl, mock.accountsChannel.v4_subaccounts_update_1, 0, null) + }, + """ { "wallet": { "account": { @@ -602,16 +736,28 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - { - }, - ) - - test( - { - perp.socket(testWsUrl, mock.accountsChannel.v4_subaccounts_update_2, 0, null) - }, - """ + """.trimIndent(), + { + }, + ) + } + + if (perp.staticTyping) { + perp.socket(testWsUrl, mock.accountsChannel.v4_subaccounts_update_2, 0, null) + + val account = perp.internalState.wallet.account + val subaccount = account.subaccounts[0] + val calculated = subaccount?.calculated?.get(CalculationPeriod.current) + assertEquals(calculated?.equity, 4272.436277000001) + assertEquals(calculated?.quoteBalance, 8772.436277) + val position = subaccount?.openPositions?.get("ETH-USD") + assertEquals(position?.calculated?.get(CalculationPeriod.current)?.size, -3.0) + } else { + test( + { + perp.socket(testWsUrl, mock.accountsChannel.v4_subaccounts_update_2, 0, null) + }, + """ { "wallet": { "account": { @@ -635,16 +781,25 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - { - }, - ) - - test( - { - perp.socket(testWsUrl, mock.accountsChannel.v4_subaccounts_update_3, 0, null) - }, - """ + """.trimIndent(), + { + }, + ) + } + + if (perp.staticTyping) { + perp.socket(testWsUrl, mock.accountsChannel.v4_subaccounts_update_3, 0, null) + + val account = perp.internalState.wallet.account + val subaccount = account.subaccounts[0] + val order = subaccount?.orders?.firstOrNull() + assertEquals(order?.status, OrderStatus.PartiallyFilled) + } else { + test( + { + perp.socket(testWsUrl, mock.accountsChannel.v4_subaccounts_update_3, 0, null) + }, + """ { "wallet": { "account": { @@ -684,16 +839,25 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - { - }, - ) - - test( - { - perp.socket(testWsUrl, mock.accountsChannel.v4_subaccounts_update_4, 0, null) - }, - """ + """.trimIndent(), + { + }, + ) + } + + if (perp.staticTyping) { + perp.socket(testWsUrl, mock.accountsChannel.v4_subaccounts_update_4, 0, null) + + val account = perp.internalState.wallet.account + val subaccount = account.subaccounts[0] + val order = subaccount?.orders?.firstOrNull() + assertEquals(order?.status, OrderStatus.Filled) + } else { + test( + { + perp.socket(testWsUrl, mock.accountsChannel.v4_subaccounts_update_4, 0, null) + }, + """ { "wallet": { "account": { @@ -733,16 +897,25 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - { - }, - ) - - test( - { - perp.socket(testWsUrl, mock.accountsChannel.v4_subaccounts_update_5, 0, null) - }, - """ + """.trimIndent(), + { + }, + ) + } + + if (perp.staticTyping) { + perp.socket(testWsUrl, mock.accountsChannel.v4_subaccounts_update_5, 0, null) + + val account = perp.internalState.wallet.account + val subaccount = account.subaccounts[0] + val order = subaccount?.orders?.firstOrNull() + assertEquals(order?.status, OrderStatus.Filled) + } else { + test( + { + perp.socket(testWsUrl, mock.accountsChannel.v4_subaccounts_update_5, 0, null) + }, + """ { "wallet": { "account": { @@ -782,16 +955,25 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - { - }, - ) - - test( - { - perp.socket(testWsUrl, mock.accountsChannel.v4_subaccounts_update_6, 0, null) - }, - """ + """.trimIndent(), + { + }, + ) + } + + if (perp.staticTyping) { + perp.socket(testWsUrl, mock.accountsChannel.v4_subaccounts_update_6, 0, null) + + val account = perp.internalState.wallet.account + val subaccount = account.subaccounts[0] + val order = subaccount?.orders?.firstOrNull() + assertEquals(order?.status, OrderStatus.Filled) + } else { + test( + { + perp.socket(testWsUrl, mock.accountsChannel.v4_subaccounts_update_6, 0, null) + }, + """ { "wallet": { "account": { @@ -831,27 +1013,44 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - { - }, - ) + """.trimIndent(), + { + }, + ) + } } private fun testAdjustedMarginFraction() { - test({ - perp.trade("CROSS", TradeInputField.marginMode, 0) - }, null) - - test( - { - perp.socket( - mock.socketUrl, - mock.marketsChannel.v4_subscribed_for_adjusted_mf_calculation, - 0, - null, - ) - }, - """ + if (perp.staticTyping) { + perp.socket( + url = mock.socketUrl, + jsonString = mock.marketsChannel.v4_subscribed_for_adjusted_mf_calculation, + subaccountNumber = 0, + height = null, + ) + + val account = perp.internalState.wallet.account + val subaccount = account.subaccounts[0] + val ethPosition = subaccount?.openPositions?.get("ETH-USD") + assertEquals(ethPosition?.calculated?.get(CalculationPeriod.current)?.adjustedImf, 0.05) + assertEquals(ethPosition?.calculated?.get(CalculationPeriod.post)?.adjustedImf, 0.05) + assertEquals(ethPosition?.calculated?.get(CalculationPeriod.current)?.adjustedMmf, 0.03) + assertEquals(ethPosition?.calculated?.get(CalculationPeriod.post)?.adjustedMmf, 0.03) + assertEquals(ethPosition?.calculated?.get(CalculationPeriod.current)?.liquidationPrice, 2838.976141423949) + assertEquals(ethPosition?.calculated?.get(CalculationPeriod.post)?.liquidationPrice, 2829.267403559871) + val btcPosition = subaccount?.openPositions?.get("BTC-USD") + assertEquals(btcPosition?.calculated?.get(CalculationPeriod.post)?.liquidationPrice, 64878.02210679612) + } else { + test( + { + perp.socket( + mock.socketUrl, + mock.marketsChannel.v4_subscribed_for_adjusted_mf_calculation, + 0, + null, + ) + }, + """ { "wallet": { "account": { @@ -891,8 +1090,9 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } fun testConditional() { @@ -916,11 +1116,18 @@ open class V4TradeInputTests : V4BaseTests() { perp.trade("900.0", TradeInputField.limitPrice, 0) }, null) - test( - { - perp.trade("1000.0", TradeInputField.triggerPrice, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("1000.0", TradeInputField.triggerPrice, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.price?.triggerPrice, 1000.0) + assertEquals(trade.price?.limitPrice, 900.0) + } else { + test( + { + perp.trade("1000.0", TradeInputField.triggerPrice, 0) + }, + """ { "input": { "trade": { @@ -931,8 +1138,9 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } test( { @@ -964,28 +1172,41 @@ open class V4TradeInputTests : V4BaseTests() { """.trimMargin(), ) - test( - { - perp.trade("STOP_MARKET", TradeInputField.type, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("STOP_MARKET", TradeInputField.type, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.summary?.payloadPrice, 949.5770392749245) + } else { + test( + { + perp.trade("STOP_MARKET", TradeInputField.type, 0) + }, + """ { "input": { "trade": { "summary": { - "payloadPrice": 1000.0 + "payloadPrice": 950.0 } } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } - test( - { - perp.trade("2000.0", TradeInputField.triggerPrice, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("2000.0", TradeInputField.triggerPrice, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.price?.triggerPrice, 2000.0) + } else { + test( + { + perp.trade("2000.0", TradeInputField.triggerPrice, 0) + }, + """ { "input": { "trade": { @@ -995,14 +1216,21 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } - test( - { - perp.trade("TAKE_PROFIT_MARKET", TradeInputField.type, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("TAKE_PROFIT_MARKET", TradeInputField.type, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.summary?.payloadPrice, 1899.154078549849) + } else { + test( + { + perp.trade("TAKE_PROFIT_MARKET", TradeInputField.type, 0) + }, + """ { "input": { "trade": { @@ -1012,20 +1240,27 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } fun testReduceOnly() { - test({ - perp.trade("CROSS", TradeInputField.marginMode, 0) - }, null) + if (perp.staticTyping) { + perp.trade("MARKET", TradeInputField.type, 0) - test( - { - perp.trade("MARKET", TradeInputField.type, 0) - }, - """ + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.Market) + assertEquals(trade.options.needsReduceOnly, true) + assertEquals(trade.options.needsPostOnly, false) + assertEquals(trade.options.reduceOnlyTooltip, null) + assertEquals(trade.options.postOnlyTooltip, null) + } else { + test( + { + perp.trade("MARKET", TradeInputField.type, 0) + }, + """ { "input": { "current": "trade", @@ -1040,15 +1275,26 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } perp.trade("LIMIT", TradeInputField.type, 0) - test( - { - perp.trade("GTT", TradeInputField.timeInForceType, 0) - }, - """ + + if (perp.staticTyping) { + perp.trade("GTT", TradeInputField.timeInForceType, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.Limit) + assertEquals(trade.options.needsReduceOnly, false) + assertEquals(trade.options.needsPostOnly, true) + assertEquals(trade.options.postOnlyTooltip, null) + } else { + test( + { + perp.trade("GTT", TradeInputField.timeInForceType, 0) + }, + """ { "input": { "current": "trade", @@ -1062,16 +1308,27 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) - - test( - { - perp.trade("IOC", TradeInputField.timeInForceType, 0) - }, - """ + """.trimIndent(), + ) + } + + if (perp.staticTyping) { + perp.trade("IOC", TradeInputField.timeInForceType, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.Limit) + assertEquals(trade.options.needsReduceOnly, true) + assertEquals(trade.options.needsPostOnly, false) + assertEquals(trade.options.reduceOnlyTooltip, null) + assertEquals(trade.options.postOnlyTooltip?.bodyStringKey, "GENERAL.TRADE.POST_ONLY_TIMEINFORCE_GTT.BODY") + } else { + test( { - "input": { + perp.trade("IOC", TradeInputField.timeInForceType, 0) + }, + """ + { + "input": { "current": "trade", "trade": { "type": "LIMIT", @@ -1084,16 +1341,26 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } perp.trade("STOP_LIMIT", TradeInputField.type, 0) - test( - { - perp.trade("DEFAULT", TradeInputField.execution, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("DEFAULT", TradeInputField.execution, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.StopLimit) + assertEquals(trade.options.needsReduceOnly, false) + assertEquals(trade.options.needsPostOnly, false) + assertEquals(trade.options.postOnlyTooltip, null) + } else { + test( + { + perp.trade("DEFAULT", TradeInputField.execution, 0) + }, + """ { "input": { "current": "trade", @@ -1107,14 +1374,25 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) - - test( - { - perp.trade("IOC", TradeInputField.execution, 0) - }, - """ + """.trimIndent(), + ) + } + + if (perp.staticTyping) { + perp.trade("IOC", TradeInputField.execution, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.StopLimit) + assertEquals(trade.options.needsReduceOnly, true) + assertEquals(trade.options.needsPostOnly, false) + assertEquals(trade.options.reduceOnlyTooltip, null) + assertEquals(trade.options.postOnlyTooltip, null) + } else { + test( + { + perp.trade("IOC", TradeInputField.execution, 0) + }, + """ { "input": { "current": "trade", @@ -1129,14 +1407,24 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) - - test( - { - perp.trade("POST_ONLY", TradeInputField.execution, 0) - }, - """ + """.trimIndent(), + ) + } + + if (perp.staticTyping) { + perp.trade("POST_ONLY", TradeInputField.execution, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.StopLimit) + assertEquals(trade.options.needsReduceOnly, false) + assertEquals(trade.options.needsPostOnly, false) + assertEquals(trade.options.postOnlyTooltip, null) + } else { + test( + { + perp.trade("POST_ONLY", TradeInputField.execution, 0) + }, + """ { "input": { "current": "trade", @@ -1150,16 +1438,26 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } perp.trade("TAKE_PROFIT", TradeInputField.type, 0) - test( - { - perp.trade("DEFAULT", TradeInputField.execution, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("DEFAULT", TradeInputField.execution, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.TakeProfitLimit) + assertEquals(trade.options.needsReduceOnly, false) + assertEquals(trade.options.needsPostOnly, false) + assertEquals(trade.options.postOnlyTooltip, null) + } else { + test( + { + perp.trade("DEFAULT", TradeInputField.execution, 0) + }, + """ { "input": { "current": "trade", @@ -1173,14 +1471,25 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) - - test( - { - perp.trade("IOC", TradeInputField.execution, 0) - }, - """ + """.trimIndent(), + ) + } + + if (perp.staticTyping) { + perp.trade("IOC", TradeInputField.execution, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.TakeProfitLimit) + assertEquals(trade.options.needsReduceOnly, true) + assertEquals(trade.options.needsPostOnly, false) + assertEquals(trade.options.reduceOnlyTooltip, null) + assertEquals(trade.options.postOnlyTooltip, null) + } else { + test( + { + perp.trade("IOC", TradeInputField.execution, 0) + }, + """ { "input": { "current": "trade", @@ -1195,14 +1504,24 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) - - test( - { - perp.trade("POST_ONLY", TradeInputField.execution, 0) - }, - """ + """.trimIndent(), + ) + } + + if (perp.staticTyping) { + perp.trade("POST_ONLY", TradeInputField.execution, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.TakeProfitLimit) + assertEquals(trade.options.needsReduceOnly, false) + assertEquals(trade.options.needsPostOnly, false) + assertEquals(trade.options.postOnlyTooltip, null) + } else { + test( + { + perp.trade("POST_ONLY", TradeInputField.execution, 0) + }, + """ { "input": { "current": "trade", @@ -1216,16 +1535,27 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } perp.trade("STOP_MARKET", TradeInputField.type, 0) - test( - { - perp.trade("IOC", TradeInputField.execution, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("IOC", TradeInputField.execution, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.StopMarket) + assertEquals(trade.options.needsReduceOnly, true) + assertEquals(trade.options.needsPostOnly, false) + assertEquals(trade.options.reduceOnlyTooltip, null) + assertEquals(trade.options.postOnlyTooltip, null) + } else { + test( + { + perp.trade("IOC", TradeInputField.execution, 0) + }, + """ { "input": { "current": "trade", @@ -1240,16 +1570,27 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } perp.trade("TAKE_PROFIT_MARKET", TradeInputField.type, 0) - test( - { - perp.trade("IOC", TradeInputField.execution, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("IOC", TradeInputField.execution, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.TakeProfitMarket) + assertEquals(trade.options.needsReduceOnly, true) + assertEquals(trade.options.needsPostOnly, false) + assertEquals(trade.options.reduceOnlyTooltip, null) + assertEquals(trade.options.postOnlyTooltip, null) + } else { + test( + { + perp.trade("IOC", TradeInputField.execution, 0) + }, + """ { "input": { "current": "trade", @@ -1264,8 +1605,9 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } private fun testExecution() { @@ -1285,11 +1627,18 @@ open class V4TradeInputTests : V4BaseTests() { perp.trade("STOP_LIMIT", TradeInputField.type, 0) perp.trade("DEFAULT", TradeInputField.execution, 0) - test( - { - perp.trade("STOP_MARKET", TradeInputField.type, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("STOP_MARKET", TradeInputField.type, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.StopMarket) + assertEquals(trade.execution, "IOC") + } else { + test( + { + perp.trade("STOP_MARKET", TradeInputField.type, 0) + }, + """ { "input": { "current": "trade", @@ -1299,16 +1648,25 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } perp.trade("STOP_LIMIT", TradeInputField.type, 0) perp.trade("IOC", TradeInputField.execution, 0) - test( - { - perp.trade("STOP_MARKET", TradeInputField.type, 0) - }, - """ + + if (perp.staticTyping) { + perp.trade("STOP_MARKET", TradeInputField.type, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.StopMarket) + assertEquals(trade.execution, "IOC") + } else { + test( + { + perp.trade("STOP_MARKET", TradeInputField.type, 0) + }, + """ { "input": { "current": "trade", @@ -1318,17 +1676,25 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } perp.trade("STOP_LIMIT", TradeInputField.type, 0) perp.trade("POST_ONLY", TradeInputField.execution, 0) - test( - { - perp.trade("STOP_MARKET", TradeInputField.type, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("STOP_MARKET", TradeInputField.type, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.StopMarket) + assertEquals(trade.execution, "IOC") + } else { + test( + { + perp.trade("STOP_MARKET", TradeInputField.type, 0) + }, + """ { "input": { "current": "trade", @@ -1338,19 +1704,27 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } private fun testExecutionStopLimitToTakeProfit() { perp.trade("STOP_LIMIT", TradeInputField.type, 0) perp.trade("DEFAULT", TradeInputField.execution, 0) - test( - { - perp.trade("TAKE_PROFIT", TradeInputField.type, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("TAKE_PROFIT", TradeInputField.type, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.TakeProfitLimit) + assertEquals(trade.execution, "DEFAULT") + } else { + test( + { + perp.trade("TAKE_PROFIT", TradeInputField.type, 0) + }, + """ { "input": { "current": "trade", @@ -1360,16 +1734,25 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } perp.trade("STOP_LIMIT", TradeInputField.type, 0) perp.trade("IOC", TradeInputField.execution, 0) - test( - { - perp.trade("TAKE_PROFIT", TradeInputField.type, 0) - }, - """ + + if (perp.staticTyping) { + perp.trade("TAKE_PROFIT", TradeInputField.type, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.TakeProfitLimit) + assertEquals(trade.execution, "IOC") + } else { + test( + { + perp.trade("TAKE_PROFIT", TradeInputField.type, 0) + }, + """ { "input": { "current": "trade", @@ -1379,17 +1762,25 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } perp.trade("STOP_LIMIT", TradeInputField.type, 0) perp.trade("POST_ONLY", TradeInputField.execution, 0) - test( - { - perp.trade("TAKE_PROFIT", TradeInputField.type, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("TAKE_PROFIT", TradeInputField.type, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.TakeProfitLimit) + assertEquals(trade.execution, "POST_ONLY") + } else { + test( + { + perp.trade("TAKE_PROFIT", TradeInputField.type, 0) + }, + """ { "input": { "current": "trade", @@ -1399,19 +1790,27 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } private fun testExecutionStopLimitToTakeProfitMarket() { perp.trade("STOP_LIMIT", TradeInputField.type, 0) perp.trade("DEFAULT", TradeInputField.execution, 0) - test( - { - perp.trade("TAKE_PROFIT_MARKET", TradeInputField.type, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("TAKE_PROFIT_MARKET", TradeInputField.type, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.TakeProfitMarket) + assertEquals(trade.execution, "IOC") + } else { + test( + { + perp.trade("TAKE_PROFIT_MARKET", TradeInputField.type, 0) + }, + """ { "input": { "current": "trade", @@ -1421,17 +1820,26 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } perp.trade("STOP_LIMIT", TradeInputField.type, 0) perp.trade("IOC", TradeInputField.execution, 0) - test( - { - perp.trade("TAKE_PROFIT_MARKET", TradeInputField.type, 0) - // should change to IOC - }, - """ + + if (perp.staticTyping) { + perp.trade("TAKE_PROFIT_MARKET", TradeInputField.type, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.TakeProfitMarket) + assertEquals(trade.execution, "IOC") + } else { + test( + { + perp.trade("TAKE_PROFIT_MARKET", TradeInputField.type, 0) + // should change to IOC + }, + """ { "input": { "current": "trade", @@ -1441,17 +1849,25 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } perp.trade("STOP_LIMIT", TradeInputField.type, 0) perp.trade("POST_ONLY", TradeInputField.execution, 0) - test( - { - perp.trade("TAKE_PROFIT_MARKET", TradeInputField.type, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("TAKE_PROFIT_MARKET", TradeInputField.type, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.TakeProfitMarket) + assertEquals(trade.execution, "IOC") + } else { + test( + { + perp.trade("TAKE_PROFIT_MARKET", TradeInputField.type, 0) + }, + """ { "input": { "current": "trade", @@ -1461,8 +1877,9 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } private fun testExecutionStopMarket() { @@ -1475,11 +1892,18 @@ open class V4TradeInputTests : V4BaseTests() { perp.trade("STOP_MARKET", TradeInputField.type, 0) perp.trade("IOC", TradeInputField.execution, 0) - test( - { - perp.trade("STOP_LIMIT", TradeInputField.type, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("STOP_LIMIT", TradeInputField.type, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.StopLimit) + assertEquals(trade.execution, "IOC") + } else { + test( + { + perp.trade("STOP_LIMIT", TradeInputField.type, 0) + }, + """ { "input": { "current": "trade", @@ -1489,18 +1913,27 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } private fun testExecutionStopMarketToTakeProfit() { perp.trade("STOP_MARKET", TradeInputField.type, 0) perp.trade("IOC", TradeInputField.execution, 0) - test( - { - perp.trade("TAKE_PROFIT", TradeInputField.type, 0) - }, - """ + + if (perp.staticTyping) { + perp.trade("TAKE_PROFIT", TradeInputField.type, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.TakeProfitLimit) + assertEquals(trade.execution, "IOC") + } else { + test( + { + perp.trade("TAKE_PROFIT", TradeInputField.type, 0) + }, + """ { "input": { "current": "trade", @@ -1510,18 +1943,27 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } private fun testExecutionStopMarketToTakeProfitMarket() { perp.trade("STOP_MARKET", TradeInputField.type, 0) perp.trade("IOC", TradeInputField.execution, 0) - test( - { - perp.trade("TAKE_PROFIT_MARKET", TradeInputField.type, 0) - }, - """ + + if (perp.staticTyping) { + perp.trade("TAKE_PROFIT_MARKET", TradeInputField.type, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.TakeProfitMarket) + assertEquals(trade.execution, "IOC") + } else { + test( + { + perp.trade("TAKE_PROFIT_MARKET", TradeInputField.type, 0) + }, + """ { "input": { "current": "trade", @@ -1531,8 +1973,9 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } private fun testExecutionTakeProfit() { @@ -1545,11 +1988,18 @@ open class V4TradeInputTests : V4BaseTests() { perp.trade("TAKE_PROFIT", TradeInputField.type, 0) perp.trade("DEFAULT", TradeInputField.execution, 0) - test( - { - perp.trade("STOP_MARKET", TradeInputField.type, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("STOP_MARKET", TradeInputField.type, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.StopMarket) + assertEquals(trade.execution, "IOC") + } else { + test( + { + perp.trade("STOP_MARKET", TradeInputField.type, 0) + }, + """ { "input": { "current": "trade", @@ -1559,17 +2009,26 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } perp.trade("TAKE_PROFIT", TradeInputField.type, 0) perp.trade("IOC", TradeInputField.execution, 0) - test( - { - perp.trade("STOP_MARKET", TradeInputField.type, 0) - // should change to IOC - }, - """ + + if (perp.staticTyping) { + perp.trade("STOP_MARKET", TradeInputField.type, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.StopMarket) + assertEquals(trade.execution, "IOC") + } else { + test( + { + perp.trade("STOP_MARKET", TradeInputField.type, 0) + // should change to IOC + }, + """ { "input": { "current": "trade", @@ -1579,17 +2038,25 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } perp.trade("TAKE_PROFIT", TradeInputField.type, 0) perp.trade("POST_ONLY", TradeInputField.execution, 0) - test( - { - perp.trade("STOP_MARKET", TradeInputField.type, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("STOP_MARKET", TradeInputField.type, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.StopMarket) + assertEquals(trade.execution, "IOC") + } else { + test( + { + perp.trade("STOP_MARKET", TradeInputField.type, 0) + }, + """ { "input": { "current": "trade", @@ -1599,19 +2066,27 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } private fun testExecutionTakeProfitToStopLimit() { perp.trade("TAKE_PROFIT", TradeInputField.type, 0) perp.trade("DEFAULT", TradeInputField.execution, 0) - test( - { - perp.trade("STOP_LIMIT", TradeInputField.type, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("STOP_LIMIT", TradeInputField.type, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.StopLimit) + assertEquals(trade.execution, "DEFAULT") + } else { + test( + { + perp.trade("STOP_LIMIT", TradeInputField.type, 0) + }, + """ { "input": { "current": "trade", @@ -1621,16 +2096,25 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } perp.trade("TAKE_PROFIT", TradeInputField.type, 0) perp.trade("IOC", TradeInputField.execution, 0) - test( - { - perp.trade("STOP_LIMIT", TradeInputField.type, 0) - }, - """ + + if (perp.staticTyping) { + perp.trade("STOP_LIMIT", TradeInputField.type, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.StopLimit) + assertEquals(trade.execution, "IOC") + } else { + test( + { + perp.trade("STOP_LIMIT", TradeInputField.type, 0) + }, + """ { "input": { "current": "trade", @@ -1640,17 +2124,25 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } perp.trade("TAKE_PROFIT", TradeInputField.type, 0) perp.trade("POST_ONLY", TradeInputField.execution, 0) - test( - { - perp.trade("STOP_LIMIT", TradeInputField.type, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("STOP_LIMIT", TradeInputField.type, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.StopLimit) + assertEquals(trade.execution, "POST_ONLY") + } else { + test( + { + perp.trade("STOP_LIMIT", TradeInputField.type, 0) + }, + """ { "input": { "current": "trade", @@ -1660,19 +2152,27 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } private fun testExecutionTakeProfitToTakeProfitMarket() { perp.trade("TAKE_PROFIT", TradeInputField.type, 0) perp.trade("DEFAULT", TradeInputField.execution, 0) - test( - { - perp.trade("TAKE_PROFIT_MARKET", TradeInputField.type, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("TAKE_PROFIT_MARKET", TradeInputField.type, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.TakeProfitMarket) + assertEquals(trade.execution, "IOC") + } else { + test( + { + perp.trade("TAKE_PROFIT_MARKET", TradeInputField.type, 0) + }, + """ { "input": { "current": "trade", @@ -1682,16 +2182,25 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } perp.trade("TAKE_PROFIT", TradeInputField.type, 0) perp.trade("IOC", TradeInputField.execution, 0) - test( - { - perp.trade("TAKE_PROFIT_MARKET", TradeInputField.type, 0) - }, - """ + + if (perp.staticTyping) { + perp.trade("TAKE_PROFIT_MARKET", TradeInputField.type, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.TakeProfitMarket) + assertEquals(trade.execution, "IOC") + } else { + test( + { + perp.trade("TAKE_PROFIT_MARKET", TradeInputField.type, 0) + }, + """ { "input": { "current": "trade", @@ -1701,17 +2210,25 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } perp.trade("TAKE_PROFIT", TradeInputField.type, 0) perp.trade("POST_ONLY", TradeInputField.execution, 0) - test( - { - perp.trade("TAKE_PROFIT_MARKET", TradeInputField.type, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("TAKE_PROFIT_MARKET", TradeInputField.type, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.TakeProfitMarket) + assertEquals(trade.execution, "IOC") + } else { + test( + { + perp.trade("TAKE_PROFIT_MARKET", TradeInputField.type, 0) + }, + """ { "input": { "current": "trade", @@ -1721,8 +2238,9 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } private fun testExecutionTakeProfitMarket() { @@ -1735,11 +2253,18 @@ open class V4TradeInputTests : V4BaseTests() { perp.trade("TAKE_PROFIT_MARKET", TradeInputField.type, 0) perp.trade("IOC", TradeInputField.execution, 0) - test( - { - perp.trade("STOP_LIMIT", TradeInputField.type, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("STOP_LIMIT", TradeInputField.type, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.StopLimit) + assertEquals(trade.execution, "IOC") + } else { + test( + { + perp.trade("STOP_LIMIT", TradeInputField.type, 0) + }, + """ { "input": { "current": "trade", @@ -1749,18 +2274,27 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } private fun testExecutionTakeProfitMarketToTakeProfit() { perp.trade("TAKE_PROFIT_MARKET", TradeInputField.type, 0) perp.trade("IOC", TradeInputField.execution, 0) - test( - { - perp.trade("TAKE_PROFIT", TradeInputField.type, 0) - }, - """ + + if (perp.staticTyping) { + perp.trade("TAKE_PROFIT", TradeInputField.type, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.TakeProfitLimit) + assertEquals(trade.execution, "IOC") + } else { + test( + { + perp.trade("TAKE_PROFIT", TradeInputField.type, 0) + }, + """ { "input": { "current": "trade", @@ -1770,18 +2304,27 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } private fun testExecutionTakeProfitMarketToStopMarket() { perp.trade("TAKE_PROFIT_MARKET", TradeInputField.type, 0) perp.trade("IOC", TradeInputField.execution, 0) - test( - { - perp.trade("STOP_MARKET", TradeInputField.type, 0) - }, - """ + + if (perp.staticTyping) { + perp.trade("STOP_MARKET", TradeInputField.type, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.StopMarket) + assertEquals(trade.execution, "IOC") + } else { + test( + { + perp.trade("STOP_MARKET", TradeInputField.type, 0) + }, + """ { "input": { "current": "trade", @@ -1791,7 +2334,8 @@ open class V4TradeInputTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } } diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4WithdrawalSafetyChecksTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4WithdrawalSafetyChecksTests.kt index 13c472634..85a734e0e 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4WithdrawalSafetyChecksTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4WithdrawalSafetyChecksTests.kt @@ -1,5 +1,6 @@ package exchange.dydx.abacus.payload.v4 +import exchange.dydx.abacus.output.input.ErrorType import exchange.dydx.abacus.output.input.TransferType import exchange.dydx.abacus.responses.ParsingError import exchange.dydx.abacus.responses.ParsingException @@ -28,7 +29,10 @@ class V4WithdrawalSafetyChecksTests : V4BaseTests() { setup() if (perp.staticTyping) { perp.parseOnChainWithdrawalGating(mock.v4WithdrawalSafetyChecksMock.withdrawal_and_transfer_gating_status_data) - assertEquals(perp.internalState.configs.withdrawalGating?.withdrawalsAndTransfersUnblockedAtBlock, 16750) + assertEquals( + perp.internalState.configs.withdrawalGating?.withdrawalsAndTransfersUnblockedAtBlock, + 16750, + ) } else { test( { @@ -202,46 +206,37 @@ class V4WithdrawalSafetyChecksTests : V4BaseTests() { @Test fun testCapacity() { setup() + perp.transfer("WITHDRAWAL", TransferInputField.type) perp.transfer("1235.0", TransferInputField.usdcSize) + if (perp.staticTyping) { - test( - { - perp.parseOnChainWithdrawalCapacity(mock.v4WithdrawalSafetyChecksMock.withdrawal_capacity_by_denom_data_daily_less_than_weekly) - }, - """ - { - "input": { - "errors": [ - { - "type": "ERROR", - "code": "", - "linkText": "APP.GENERAL.LEARN_MORE_ARROW", - "resources": { - "title": { - "stringKey": "WARNINGS.ACCOUNT_FUND_MANAGEMENT.WITHDRAWAL_LIMIT_OVER_TITLE" - }, - "text": { - "stringKey": "WARNINGS.ACCOUNT_FUND_MANAGEMENT.WITHDRAWAL_LIMIT_OVER_DESCRIPTION", - "params": [ - { - "value": 1234.567891, - "format": "price", - "key": "USDC_LIMIT" - } - ] - }, - "action": { - "stringKey": "WARNINGS.ACCOUNT_FUND_MANAGEMENT.WITHDRAWAL_LIMIT_OVER_ACTION" - } - } - } - ] - } - } - """.trimIndent(), + perp.parseOnChainWithdrawalCapacity(mock.v4WithdrawalSafetyChecksMock.withdrawal_capacity_by_denom_data_daily_less_than_weekly) + val errors = perp.internalState.input.errors + assertEquals(errors?.size, 1) + val error = errors?.get(0) + assertEquals(error?.type, ErrorType.error) + assertEquals(error?.code, "") + assertEquals(error?.linkText, "APP.GENERAL.LEARN_MORE_ARROW") + val resources = error?.resources + assertEquals( + resources?.title?.stringKey, + "WARNINGS.ACCOUNT_FUND_MANAGEMENT.WITHDRAWAL_LIMIT_OVER_TITLE", + ) + assertEquals( + resources?.text?.stringKey, + "WARNINGS.ACCOUNT_FUND_MANAGEMENT.WITHDRAWAL_LIMIT_OVER_DESCRIPTION", + ) + assertEquals(resources?.text?.params?.size, 1) + val param = resources?.text?.params?.get(0) + assertEquals(param?.value, "1234.567891") + assertEquals(param?.format, "price") + assertEquals(param?.key, "USDC_LIMIT") + + assertEquals( + parser.asDouble(perp.internalState.configs.withdrawalCapacity?.maxWithdrawalCapacity), + 1234.567891, ) - assertEquals(parser.asDouble(perp.internalState.configs.withdrawalCapacity?.maxWithdrawalCapacity), 1234.567891) assertEquals(perp.internalState.configs.withdrawalCapacity?.capacity, "1234567891") } else { test( diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/tests/payloads/AccountsChannelMock.kt b/src/commonTest/kotlin/exchange.dydx.abacus/tests/payloads/AccountsChannelMock.kt index d35caf423..90855aac4 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/tests/payloads/AccountsChannelMock.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/tests/payloads/AccountsChannelMock.kt @@ -1195,7 +1195,6 @@ internal class AccountsChannelMock { "equity": "100000.000000", "freeCollateral": "100000.000000", "openPerpetualPositions": {}, - "quoteBalance": "100000.000000", "marginEnabled": true }, { "address": "cosmos1fq8q55896ljfjj7v3x0qd0z3sr78wmes940uhm", @@ -1203,7 +1202,6 @@ internal class AccountsChannelMock { "equity": "100000.000000", "freeCollateral": "100000.000000", "openPerpetualPositions": {}, - "quoteBalance": "100000.000000", "marginEnabled": true }, { "address": "cosmos1fq8q55896ljfjj7v3x0qd0z3sr78wmes940uhm", @@ -1211,7 +1209,6 @@ internal class AccountsChannelMock { "equity": "100000.000000", "freeCollateral": "100000.000000", "openPerpetualPositions": {}, - "quoteBalance": "100000.000000", "marginEnabled": true }, { "address": "cosmos1fq8q55896ljfjj7v3x0qd0z3sr78wmes940uhm", @@ -1219,7 +1216,6 @@ internal class AccountsChannelMock { "equity": "100000.000000", "freeCollateral": "100000.000000", "openPerpetualPositions": {}, - "quoteBalance": "100000.000000", "marginEnabled": true }, { "address": "cosmos1fq8q55896ljfjj7v3x0qd0z3sr78wmes940uhm", @@ -1227,7 +1223,6 @@ internal class AccountsChannelMock { "equity": "100000.000000", "freeCollateral": "100000.000000", "openPerpetualPositions": {}, - "quoteBalance": "100000.000000", "marginEnabled": true }, { "address": "cosmos1fq8q55896ljfjj7v3x0qd0z3sr78wmes940uhm", @@ -1235,7 +1230,6 @@ internal class AccountsChannelMock { "equity": "100000.000000", "freeCollateral": "100000.000000", "openPerpetualPositions": {}, - "quoteBalance": "100000.000000", "marginEnabled": true }, { "address": "cosmos1fq8q55896ljfjj7v3x0qd0z3sr78wmes940uhm", @@ -1243,7 +1237,6 @@ internal class AccountsChannelMock { "equity": "100000.000000", "freeCollateral": "100000.000000", "openPerpetualPositions": {}, - "quoteBalance": "100000.000000", "marginEnabled": true }, { "address": "cosmos1fq8q55896ljfjj7v3x0qd0z3sr78wmes940uhm", @@ -1251,7 +1244,6 @@ internal class AccountsChannelMock { "equity": "100000.000000", "freeCollateral": "100000.000000", "openPerpetualPositions": {}, - "quoteBalance": "100000.000000", "marginEnabled": true }, { "address": "cosmos1fq8q55896ljfjj7v3x0qd0z3sr78wmes940uhm", @@ -1259,7 +1251,6 @@ internal class AccountsChannelMock { "equity": "100000.000000", "freeCollateral": "100000.000000", "openPerpetualPositions": {}, - "quoteBalance": "100000.000000", "marginEnabled": true }] } @@ -1749,7 +1740,14 @@ internal class AccountsChannelMock { "netFunding":"0" } }, - "quoteBalance":"68257.215192", + "assetPositions": { + "USDC": { + "symbol": "USDC", + "side": "LONG", + "size": "68257.215192", + "assetId": "0" + } + }, "marginEnabled":true }, "orders":[ @@ -1852,7 +1850,7 @@ internal class AccountsChannelMock { { "id":"b812bea8-29d3-5841-9549-caa072f6f8a8", "market": "ETH-USD", - "ticker":"ETH-USD", + "ticker":"ETH-USD", "subaccountId":"660efb4c-5472-5119-8c17-65cf702ccaea", "clientId":"2194126268", "clobPairId":"1", @@ -1936,7 +1934,6 @@ internal class AccountsChannelMock { }, { "id": "770933a5-0293-5aca-8a01-d9c4030d776d", - "ticker":"ETH-USD", "subaccountId": "e470a747-3aa0-543e-aafa-0bd27d568901", "clientId": "852785216", "clobPairId": "1", @@ -2037,7 +2034,7 @@ internal class AccountsChannelMock { "subaccountNumber":0, "positionId":"24a26508-9d45-5b4c-a13b-31f6e9780ecc", "assetId":"0", - "denom":"USDC", + "symbol":"USDC", "side":"LONG", "size":"7250.506704" } @@ -2076,7 +2073,7 @@ internal class AccountsChannelMock { "subaccountNumber":0, "positionId":"24a26508-9d45-5b4c-a13b-31f6e9780ecc", "assetId":"0", - "denom":"USDC", + "symbol":"USDC", "side":"LONG", "size":"8772.436277" } diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/tests/payloads/SubaccountChannelMock.kt b/src/commonTest/kotlin/exchange.dydx.abacus/tests/payloads/SubaccountChannelMock.kt index f2b8e1b25..a16b832ee 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/tests/payloads/SubaccountChannelMock.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/tests/payloads/SubaccountChannelMock.kt @@ -5355,7 +5355,7 @@ internal class SubaccountsChannelMock { "clientMetadata": "0", "createdAtHeight": "14972962", "transactionHash": "E06299AA699592C0F60ACBDA3C241AAED74A7D6986B07A1707F425072453DD4B", - "ticker": "APT-USD" + "market": "APT-USD" }, { "id": "0d473eec-93b0-5c49-94ca-b8017454d769", @@ -5374,7 +5374,7 @@ internal class SubaccountsChannelMock { "clientMetadata": "0", "createdAtHeight": "14972962", "transactionHash": "E06299AA699592C0F60ACBDA3C241AAED74A7D6986B07A1707F425072453DD4B", - "ticker": "APT-USD" + "market": "APT-USD" } ], "perpetualPositions": [ diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/validation/TradeBracketsTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/validation/TradeBracketsTests.kt index 00de24c30..f8ad8ed96 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/validation/TradeBracketsTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/validation/TradeBracketsTests.kt @@ -1,5 +1,8 @@ package exchange.dydx.abacus.validation +import exchange.dydx.abacus.output.input.ErrorType +import exchange.dydx.abacus.output.input.OrderSide +import exchange.dydx.abacus.output.input.OrderType import exchange.dydx.abacus.responses.StateResponse import exchange.dydx.abacus.state.model.TradeInputField import exchange.dydx.abacus.state.model.trade @@ -7,6 +10,7 @@ import exchange.dydx.abacus.state.model.tradeInMarket import exchange.dydx.abacus.tests.extensions.log import exchange.dydx.abacus.utils.ServerTime import kotlin.test.Test +import kotlin.test.assertEquals class TradeBracketsTests : ValidationsTests() { @Test @@ -51,11 +55,54 @@ class TradeBracketsTests : ValidationsTests() { perp.trade("900", TradeInputField.bracketsTakeProfitPrice, 0) }, null) - test( - { - perp.trade("1100", TradeInputField.bracketsStopLossPrice, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("1100", TradeInputField.bracketsStopLossPrice, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.Market) + assertEquals(trade.side, OrderSide.Buy) + assertEquals(trade.marketId, "ETH-USD") + assertEquals(trade.timeInForce, "GTT") + val summary = trade.summary!! + assertEquals(summary.price, 1000.1) + assertEquals(summary.size, 0.1) + assertEquals(summary.usdcSize, 100.01) + assertEquals(summary.total, -100.01) + assertEquals(summary.slippage, 0.0001) + assertEquals(summary.indexSlippage, 0.0001) + assertEquals(summary.filled, true) + val size = trade.size!! + assertEquals(size.size, 0.1) + assertEquals(size.usdcSize, 100.01) + + val errors = perp.internalState.input.errors + assertEquals(errors?.size, 2) + + val error = errors?.firstOrNull() + assertEquals(error?.type, ErrorType.error) + assertEquals(error?.code, "BRACKET_ORDER_TAKE_PROFIT_ABOVE_EXPECTED_PRICE") + assertEquals(error?.fields?.size, 2) + assertEquals(error?.fields?.get(0), "brackets.takeProfit.triggerPrice") + assertEquals(error?.fields?.get(1), "brackets.takeProfit.percent") + assertEquals(error?.resources?.title?.stringKey, "ERRORS.TRADE_BOX_TITLE.BRACKET_ORDER_TAKE_PROFIT_ABOVE_EXPECTED_PRICE") + assertEquals(error?.resources?.text?.stringKey, "ERRORS.TRADE_BOX.BRACKET_ORDER_TAKE_PROFIT_ABOVE_EXPECTED_PRICE") + assertEquals(error?.resources?.action?.stringKey, "APP.TRADE.ENTER_TRIGGER_PRICE") + + val error2 = errors?.get(1) + assertEquals(error2?.type, ErrorType.error) + assertEquals(error2?.code, "BRACKET_ORDER_STOP_LOSS_BELOW_EXPECTED_PRICE") + assertEquals(error2?.fields?.size, 2) + assertEquals(error2?.fields?.get(0), "brackets.stopLoss.triggerPrice") + assertEquals(error2?.fields?.get(1), "brackets.stopLoss.percent") + assertEquals(error2?.resources?.title?.stringKey, "ERRORS.TRADE_BOX_TITLE.BRACKET_ORDER_STOP_LOSS_BELOW_EXPECTED_PRICE") + assertEquals(error2?.resources?.text?.stringKey, "ERRORS.TRADE_BOX.BRACKET_ORDER_STOP_LOSS_BELOW_EXPECTED_PRICE") + assertEquals(error2?.resources?.action?.stringKey, "APP.TRADE.ENTER_TRIGGER_PRICE") + } else { + test( + { + perp.trade("1100", TradeInputField.bracketsStopLossPrice, 0) + }, + """ { "input": { "current": "trade", @@ -128,18 +175,28 @@ class TradeBracketsTests : ValidationsTests() { ] } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } test({ perp.trade("1100", TradeInputField.bracketsTakeProfitPrice, 0) }, null) - test( - { - perp.trade("900", TradeInputField.bracketsStopLossPrice, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("900", TradeInputField.bracketsStopLossPrice, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.Market) + assertEquals(trade.side, OrderSide.Buy) + assertEquals(trade.marketId, "ETH-USD") + assertEquals(trade.timeInForce, "GTT") + } else { + test( + { + perp.trade("900", TradeInputField.bracketsStopLossPrice, 0) + }, + """ { "input": { "current": "trade", @@ -152,8 +209,9 @@ class TradeBracketsTests : ValidationsTests() { "errors": null } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } test({ loadValidationsAccounts2() @@ -179,11 +237,48 @@ class TradeBracketsTests : ValidationsTests() { perp.trade("true", TradeInputField.bracketsTakeProfitReduceOnly, 0) }, null) - test( - { - perp.trade("true", TradeInputField.bracketsStopLossReduceOnly, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("true", TradeInputField.bracketsStopLossReduceOnly, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.Market) + assertEquals(trade.side, OrderSide.Sell) + assertEquals(trade.marketId, "ETH-USD") + assertEquals(trade.timeInForce, "GTT") + val summary = trade.summary!! + assertEquals(summary.price, 999.9000000000001) + assertEquals(summary.size, 0.1) + assertEquals(summary.usdcSize, 99.99000000000001) + assertEquals(summary.total, 99.99000000000001) + assertEquals(summary.slippage, 0.0001) + assertEquals(summary.indexSlippage, 0.0001) + assertEquals(summary.filled, true) + val size = trade.size!! + assertEquals(size.size, 0.1) + assertEquals(size.usdcSize, 99.99000000000001) + + val errors = perp.internalState.input.errors + assertEquals(errors?.size, 2) + + val error = errors?.firstOrNull() + assertEquals(error?.type, ErrorType.error) + assertEquals(error?.code, "WOULD_NOT_REDUCE_UNCHECK") + assertEquals(error?.fields?.size, 2) + assertEquals(error?.fields?.get(0), "brackets.takeProfit.triggerPrice") + assertEquals(error?.fields?.get(1), "brackets.takeProfit.reduceOnly") + + val error2 = errors?.get(1) + assertEquals(error2?.type, ErrorType.error) + assertEquals(error2?.code, "WOULD_NOT_REDUCE_UNCHECK") + assertEquals(error2?.fields?.size, 2) + assertEquals(error2?.fields?.get(0), "brackets.stopLoss.triggerPrice") + assertEquals(error2?.fields?.get(1), "brackets.stopLoss.reduceOnly") + } else { + test( + { + perp.trade("true", TradeInputField.bracketsStopLossReduceOnly, 0) + }, + """ { "input": { "current": "trade", @@ -234,7 +329,8 @@ class TradeBracketsTests : ValidationsTests() { ] } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } } diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/validation/TradeMarketOrderTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/validation/TradeMarketOrderTests.kt index 6055e1e51..03d4fe171 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/validation/TradeMarketOrderTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/validation/TradeMarketOrderTests.kt @@ -1,11 +1,15 @@ package exchange.dydx.abacus.validation +import exchange.dydx.abacus.output.input.ErrorType +import exchange.dydx.abacus.output.input.OrderSide +import exchange.dydx.abacus.output.input.OrderType import exchange.dydx.abacus.state.model.TradeInputField import exchange.dydx.abacus.state.model.trade import exchange.dydx.abacus.state.model.tradeInMarket import exchange.dydx.abacus.tests.extensions.log import exchange.dydx.abacus.utils.ServerTime import kotlin.test.Test +import kotlin.test.assertEquals class TradeMarketOrderTests : ValidationsTests() { @Test @@ -34,11 +38,24 @@ class TradeMarketOrderTests : ValidationsTests() { perp.trade("MARKET", TradeInputField.type, 0) }, null) - test( - { - perp.trade("BUY", TradeInputField.side, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("BUY", TradeInputField.side, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.side, OrderSide.Buy) + assertEquals(trade.type, OrderType.Market) + assertEquals(trade.marketId, "ETH-USD") + val errors = perp.internalState.input.errors + val error = errors?.firstOrNull() + assertEquals(error?.type, ErrorType.required) + assertEquals(error?.code, "REQUIRED_SIZE") + assertEquals(error?.fields?.firstOrNull(), "size.size") + } else { + test( + { + perp.trade("BUY", TradeInputField.side, 0) + }, + """ { "input": { "current": "trade", @@ -58,14 +75,38 @@ class TradeMarketOrderTests : ValidationsTests() { ] } } - """.trimIndent(), - ) - - test( - { - perp.trade("1.0", TradeInputField.size, 0) - }, - """ + """.trimIndent(), + ) + } + + if (perp.staticTyping) { + perp.trade("1.0", TradeInputField.size, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.timeInForce, "GTT") + val summary = trade.summary!! + assertEquals(summary.price, 1024.26) + assertEquals(summary.size, 1.0) + assertEquals(summary.usdcSize, 1024.26) + assertEquals(summary.total, -1024.26) + assertEquals(summary.slippage, 0.06) + assertEquals(summary.indexSlippage, 0.06) + assertEquals(summary.filled, true) + + val orderbook = trade.marketOrder!!.orderbook!! + assertEquals(orderbook.size, 3) + assertEquals(orderbook[0].size, 0.1) + assertEquals(orderbook[0].price, 1000.1) + assertEquals(orderbook[1].size, 0.5) + assertEquals(orderbook[1].price, 1000.5) + assertEquals(orderbook[2].size, 0.4) + assertEquals(orderbook[2].price, 1060.0) + } else { + test( + { + perp.trade("1.0", TradeInputField.size, 0) + }, + """ { "input": { "current": "trade", @@ -106,14 +147,43 @@ class TradeMarketOrderTests : ValidationsTests() { } } } - """.trimIndent(), - ) - - test( - { - perp.trade("10.0", TradeInputField.size, 0) - }, - """ + """.trimIndent(), + ) + } + + if (perp.staticTyping) { + perp.trade("10.0", TradeInputField.size, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.timeInForce, "GTT") + val summary = trade.summary!! + assertEquals(summary.price, 1382.0260000000003) + assertEquals(summary.size, 10.0) + assertEquals(summary.usdcSize, 13820.260000000002) + assertEquals(summary.total, -13820.260000000002) + assertEquals(summary.slippage, 0.8) + assertEquals(summary.indexSlippage, 0.8) + assertEquals(summary.filled, true) + + assertEquals(trade.size?.size, 10.0) + assertEquals(trade.size?.usdcSize, 13820.260000000002) + + val orderbook = trade.marketOrder!!.orderbook!! + assertEquals(orderbook.size, 4) + assertEquals(orderbook[0].size, 0.1) + assertEquals(orderbook[0].price, 1000.1) + assertEquals(orderbook[1].size, 0.5) + assertEquals(orderbook[1].price, 1000.5) + assertEquals(orderbook[2].size, 5.0) + assertEquals(orderbook[2].price, 1060.0) + assertEquals(orderbook[3].size, 4.4) + assertEquals(orderbook[3].price, 1800.0) + } else { + test( + { + perp.trade("10.0", TradeInputField.size, 0) + }, + """ { "input": { "current": "trade", @@ -158,7 +228,8 @@ class TradeMarketOrderTests : ValidationsTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } } diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/validation/TradePositionTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/validation/TradePositionTests.kt index 650ebeeba..33a32ef40 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/validation/TradePositionTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/validation/TradePositionTests.kt @@ -1,11 +1,16 @@ package exchange.dydx.abacus.validation +import exchange.dydx.abacus.output.input.ErrorType +import exchange.dydx.abacus.output.input.OrderSide +import exchange.dydx.abacus.output.input.OrderType import exchange.dydx.abacus.state.model.TradeInputField import exchange.dydx.abacus.state.model.trade import exchange.dydx.abacus.state.model.tradeInMarket import exchange.dydx.abacus.tests.extensions.log import exchange.dydx.abacus.utils.ServerTime import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue class TradePositionTests : ValidationsTests() { @Test @@ -53,11 +58,25 @@ class TradePositionTests : ValidationsTests() { /* This test would throw an Flip Position error when reduceOnly is supported */ - test( - { - perp.trade("110.0", TradeInputField.size, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("110.0", TradeInputField.size, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.Limit) + assertEquals(trade.side, OrderSide.Sell) + assertEquals(trade.marketId, "ETH-USD") + assertEquals(trade.timeInForce, "IOC") + val errors = perp.internalState.input.errors + val error = errors?.get(0) + assertEquals(error?.type, ErrorType.error) + assertEquals(error?.code, "ORDER_WOULD_FLIP_POSITION") + assertEquals(error?.resources?.title?.stringKey, "ERRORS.TRADE_BOX_TITLE.ORDER_WOULD_FLIP_POSITION") + } else { + test( + { + perp.trade("110.0", TradeInputField.size, 0) + }, + """ { "input": { "current": "trade", @@ -86,8 +105,9 @@ class TradePositionTests : ValidationsTests() { } } """ - .trimIndent(), - ) + .trimIndent(), + ) + } test({ perp.trade("BUY", TradeInputField.side, 0) @@ -97,11 +117,25 @@ class TradePositionTests : ValidationsTests() { perp.trade("999.0", TradeInputField.limitPrice, 0) }, null) - test( - { - perp.trade("210.0", TradeInputField.size, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("210.0", TradeInputField.size, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.Limit) + assertEquals(trade.side, OrderSide.Buy) + assertEquals(trade.marketId, "ETH-USD") + assertEquals(trade.timeInForce, "IOC") + val errors = perp.internalState.input.errors + val error = errors?.get(0) + assertEquals(error?.type, ErrorType.error) + assertEquals(error?.code, "ORDER_WOULD_FLIP_POSITION") + assertEquals(error?.resources?.title?.stringKey, "ERRORS.TRADE_BOX_TITLE.ORDER_WOULD_FLIP_POSITION") + } else { + test( + { + perp.trade("210.0", TradeInputField.size, 0) + }, + """ { "input": { "current": "trade", @@ -129,14 +163,29 @@ class TradePositionTests : ValidationsTests() { ] } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } - test( - { - perp.trade("SELL", TradeInputField.side, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("SELL", TradeInputField.side, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.Limit) + assertEquals(trade.side, OrderSide.Sell) + assertEquals(trade.marketId, "ETH-USD") + assertEquals(trade.timeInForce, "IOC") + val errors = perp.internalState.input.errors + val error = errors?.get(0) + assertEquals(error?.type, ErrorType.error) + assertEquals(error?.code, "ORDER_WOULD_FLIP_POSITION") + assertEquals(error?.resources?.title?.stringKey, "ERRORS.TRADE_BOX_TITLE.ORDER_WOULD_FLIP_POSITION") + } else { + test( + { + perp.trade("SELL", TradeInputField.side, 0) + }, + """ { "input": { "current": "trade", @@ -164,14 +213,26 @@ class TradePositionTests : ValidationsTests() { ] } } - """.trimIndent(), - ) - - test( - { - perp.trade("10", TradeInputField.size, 0) - }, - """ + """.trimIndent(), + ) + } + + if (perp.staticTyping) { + perp.trade("10", TradeInputField.size, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.Limit) + assertEquals(trade.side, OrderSide.Sell) + assertEquals(trade.marketId, "ETH-USD") + assertEquals(trade.timeInForce, "IOC") + val errors = perp.internalState.input.errors + assertTrue { errors.isNullOrEmpty() } + } else { + test( + { + perp.trade("10", TradeInputField.size, 0) + }, + """ { "input": { "current": "trade", @@ -184,7 +245,8 @@ class TradePositionTests : ValidationsTests() { "errors": null } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } } diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/validation/TradeRequiredInputTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/validation/TradeRequiredInputTests.kt index e450dc90b..b6919c67b 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/validation/TradeRequiredInputTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/validation/TradeRequiredInputTests.kt @@ -1,11 +1,15 @@ package exchange.dydx.abacus.validation +import exchange.dydx.abacus.output.input.ErrorType +import exchange.dydx.abacus.output.input.OrderType import exchange.dydx.abacus.payload.v4.V4BaseTests import exchange.dydx.abacus.state.model.TradeInputField import exchange.dydx.abacus.state.model.trade import exchange.dydx.abacus.tests.extensions.log import exchange.dydx.abacus.utils.ServerTime import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue class TradeRequiredInputTests : V4BaseTests() { @Test @@ -17,6 +21,12 @@ class TradeRequiredInputTests : V4BaseTests() { testTradeInputOnce() } + override fun setup() { + super.setup() + + perp.internalState.wallet.walletAddress = "0x1234567890" + } + private fun testTradeInputOnce() { var time = ServerTime.now() reset() @@ -54,11 +64,22 @@ class TradeRequiredInputTests : V4BaseTests() { } private fun testTradeInputMarketType() { - test( - { - perp.trade("MARKET", TradeInputField.type, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("MARKET", TradeInputField.type, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.Market) + val error = perp.internalState.input.errors?.firstOrNull() + assertEquals(error?.type, ErrorType.required) + assertEquals(error?.code, "REQUIRED_SIZE") + assertEquals(error?.fields?.firstOrNull(), "size.size") + assertEquals(error?.resources?.action?.stringKey, "APP.TRADE.ENTER_AMOUNT") + } else { + test( + { + perp.trade("MARKET", TradeInputField.type, 0) + }, + """ { "input": { "current": "trade", @@ -81,14 +102,24 @@ class TradeRequiredInputTests : V4BaseTests() { ] } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } + + if (perp.staticTyping) { + perp.trade("1.0", TradeInputField.size, 0) - test( - { - perp.trade("1.0", TradeInputField.size, 0) - }, - """ + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.Market) + val error = perp.internalState.input.errors?.firstOrNull() + assertEquals(error?.type, ErrorType.error) + assertEquals(error?.code, "MARKET_ORDER_NOT_ENOUGH_LIQUIDITY") + } else { + test( + { + perp.trade("1.0", TradeInputField.size, 0) + }, + """ { "input": { "current": "trade", @@ -103,16 +134,33 @@ class TradeRequiredInputTests : V4BaseTests() { ] } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } private fun testTradeInputStopMarketType() { - test( - { - perp.trade("STOP_MARKET", TradeInputField.type, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("STOP_MARKET", TradeInputField.type, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.StopMarket) + val error = perp.internalState.input.errors?.firstOrNull() + assertEquals(error?.type, ErrorType.required) + assertEquals(error?.code, "REQUIRED_SIZE") + assertEquals(error?.fields?.firstOrNull(), "size.size") + assertEquals(error?.resources?.action?.stringKey, "APP.TRADE.ENTER_AMOUNT") + val error2 = perp.internalState.input.errors?.get(1) + assertEquals(error2?.type, ErrorType.required) + assertEquals(error2?.code, "REQUIRED_TRIGGER_PRICE") + assertEquals(error2?.fields?.firstOrNull(), "price.triggerPrice") + assertEquals(error2?.resources?.action?.stringKey, "APP.TRADE.ENTER_TRIGGER_PRICE") + } else { + test( + { + perp.trade("STOP_MARKET", TradeInputField.type, 0) + }, + """ { "input": { "current": "trade", @@ -147,14 +195,26 @@ class TradeRequiredInputTests : V4BaseTests() { ] } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } + + if (perp.staticTyping) { + perp.trade("1.0", TradeInputField.size, 0) - test( - { - perp.trade("1.0", TradeInputField.size, 0) - }, - """ + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.StopMarket) + val error = perp.internalState.input.errors?.firstOrNull() + assertEquals(error?.type, ErrorType.required) + assertEquals(error?.code, "REQUIRED_TRIGGER_PRICE") + assertEquals(error?.fields?.firstOrNull(), "price.triggerPrice") + assertEquals(error?.resources?.action?.stringKey, "APP.TRADE.ENTER_TRIGGER_PRICE") + } else { + test( + { + perp.trade("1.0", TradeInputField.size, 0) + }, + """ { "input": { "current": "trade", @@ -177,14 +237,24 @@ class TradeRequiredInputTests : V4BaseTests() { ] } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } + + if (perp.staticTyping) { + perp.trade("1223", TradeInputField.triggerPrice, 0) - test( - { - perp.trade("1223", TradeInputField.triggerPrice, 0) - }, - """ + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.StopMarket) + val error = perp.internalState.input.errors?.firstOrNull() + assertEquals(error?.type, ErrorType.error) + assertEquals(error?.code, "TRIGGER_MUST_ABOVE_INDEX_PRICE") + } else { + test( + { + perp.trade("1223", TradeInputField.triggerPrice, 0) + }, + """ { "input": { "current": "trade", @@ -199,14 +269,23 @@ class TradeRequiredInputTests : V4BaseTests() { ] } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } + + if (perp.staticTyping) { + perp.trade("3000", TradeInputField.triggerPrice, 0) - test( - { - perp.trade("3000", TradeInputField.triggerPrice, 0) - }, - """ + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.StopMarket) + val errors = perp.internalState.input.errors + assertTrue { errors.isNullOrEmpty() } + } else { + test( + { + perp.trade("3000", TradeInputField.triggerPrice, 0) + }, + """ { "input": { "current": "trade", @@ -216,16 +295,35 @@ class TradeRequiredInputTests : V4BaseTests() { "errors": null } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } private fun testTradeInputTakeProfitMarketType() { - test( - { - perp.trade("TAKE_PROFIT_MARKET", TradeInputField.type, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("TAKE_PROFIT_MARKET", TradeInputField.type, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.TakeProfitMarket) + val errors = perp.internalState.input.errors + assertEquals(errors?.size, 2) + val error = errors?.firstOrNull() + assertEquals(error?.type, ErrorType.required) + assertEquals(error?.code, "REQUIRED_SIZE") + assertEquals(error?.fields?.firstOrNull(), "size.size") + assertEquals(error?.resources?.action?.stringKey, "APP.TRADE.ENTER_AMOUNT") + val error2 = errors?.get(1) + assertEquals(error2?.type, ErrorType.required) + assertEquals(error2?.code, "REQUIRED_TRIGGER_PRICE") + assertEquals(error2?.fields?.firstOrNull(), "price.triggerPrice") + assertEquals(error2?.resources?.action?.stringKey, "APP.TRADE.ENTER_TRIGGER_PRICE") + } else { + test( + { + perp.trade("TAKE_PROFIT_MARKET", TradeInputField.type, 0) + }, + """ { "input": { "current": "trade", @@ -260,14 +358,28 @@ class TradeRequiredInputTests : V4BaseTests() { ] } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } - test( - { - perp.trade("1.0", TradeInputField.size, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("1.0", TradeInputField.size, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.TakeProfitMarket) + val errors = perp.internalState.input.errors + assertEquals(errors?.size, 1) + val error = errors?.firstOrNull() + assertEquals(error?.type, ErrorType.required) + assertEquals(error?.code, "REQUIRED_TRIGGER_PRICE") + assertEquals(error?.fields?.firstOrNull(), "price.triggerPrice") + assertEquals(error?.resources?.action?.stringKey, "APP.TRADE.ENTER_TRIGGER_PRICE") + } else { + test( + { + perp.trade("1.0", TradeInputField.size, 0) + }, + """ { "input": { "current": "trade", @@ -290,14 +402,24 @@ class TradeRequiredInputTests : V4BaseTests() { ] } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } - test( - { - perp.trade("1923", TradeInputField.triggerPrice, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("1923", TradeInputField.triggerPrice, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.TakeProfitMarket) + val error = perp.internalState.input.errors?.firstOrNull() + assertEquals(error?.type, ErrorType.error) + assertEquals(error?.code, "TRIGGER_MUST_BELOW_INDEX_PRICE") + } else { + test( + { + perp.trade("1923", TradeInputField.triggerPrice, 0) + }, + """ { "input": { "current": "trade", @@ -312,16 +434,35 @@ class TradeRequiredInputTests : V4BaseTests() { ] } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } private fun testTradeInputLimitType() { - test( - { - perp.trade("LIMIT", TradeInputField.type, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("LIMIT", TradeInputField.type, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.Limit) + val errors = perp.internalState.input.errors + assertEquals(errors?.size, 2) + val error = errors?.firstOrNull() + assertEquals(error?.type, ErrorType.required) + assertEquals(error?.code, "REQUIRED_SIZE") + assertEquals(error?.fields?.firstOrNull(), "size.size") + assertEquals(error?.resources?.action?.stringKey, "APP.TRADE.ENTER_AMOUNT") + val error2 = errors?.get(1) + assertEquals(error2?.type, ErrorType.required) + assertEquals(error2?.code, "REQUIRED_LIMIT_PRICE") + assertEquals(error2?.fields?.firstOrNull(), "price.limitPrice") + assertEquals(error2?.resources?.action?.stringKey, "APP.TRADE.ENTER_LIMIT_PRICE") + } else { + test( + { + perp.trade("LIMIT", TradeInputField.type, 0) + }, + """ { "input": { "current": "trade", @@ -356,14 +497,26 @@ class TradeRequiredInputTests : V4BaseTests() { ] } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } - test( - { - perp.trade("1.0", TradeInputField.size, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("1.0", TradeInputField.size, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.Limit) + val error = perp.internalState.input.errors?.firstOrNull() + assertEquals(error?.type, ErrorType.required) + assertEquals(error?.code, "REQUIRED_LIMIT_PRICE") + assertEquals(error?.fields?.firstOrNull(), "price.limitPrice") + assertEquals(error?.resources?.action?.stringKey, "APP.TRADE.ENTER_LIMIT_PRICE") + } else { + test( + { + perp.trade("1.0", TradeInputField.size, 0) + }, + """ { "input": { "current": "trade", @@ -386,14 +539,23 @@ class TradeRequiredInputTests : V4BaseTests() { ] } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } - test( - { - perp.trade("1223", TradeInputField.limitPrice, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("1223", TradeInputField.limitPrice, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.Limit) + val error = perp.internalState.input.errors?.firstOrNull() + assertEquals(error, null) + } else { + test( + { + perp.trade("1223", TradeInputField.limitPrice, 0) + }, + """ { "input": { "current": "trade", @@ -403,16 +565,40 @@ class TradeRequiredInputTests : V4BaseTests() { "errors": null } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } private fun testTradeInputStopLimitType() { - test( - { - perp.trade("STOP_LIMIT", TradeInputField.type, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("STOP_LIMIT", TradeInputField.type, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.StopLimit) + val errors = perp.internalState.input.errors + assertEquals(errors?.size, 3) + val error = errors?.firstOrNull() + assertEquals(error?.type, ErrorType.required) + assertEquals(error?.code, "REQUIRED_SIZE") + assertEquals(error?.fields?.firstOrNull(), "size.size") + assertEquals(error?.resources?.action?.stringKey, "APP.TRADE.ENTER_AMOUNT") + val error2 = errors?.get(1) + assertEquals(error2?.type, ErrorType.required) + assertEquals(error2?.code, "REQUIRED_LIMIT_PRICE") + assertEquals(error2?.fields?.firstOrNull(), "price.limitPrice") + assertEquals(error2?.resources?.action?.stringKey, "APP.TRADE.ENTER_LIMIT_PRICE") + val error3 = errors?.get(2) + assertEquals(error3?.type, ErrorType.required) + assertEquals(error3?.code, "REQUIRED_TRIGGER_PRICE") + assertEquals(error3?.fields?.firstOrNull(), "price.triggerPrice") + assertEquals(error3?.resources?.action?.stringKey, "APP.TRADE.ENTER_TRIGGER_PRICE") + } else { + test( + { + perp.trade("STOP_LIMIT", TradeInputField.type, 0) + }, + """ { "input": { "current": "trade", @@ -459,14 +645,33 @@ class TradeRequiredInputTests : V4BaseTests() { ] } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } + + if (perp.staticTyping) { + perp.trade("1.0", TradeInputField.size, 0) - test( - { - perp.trade("1.0", TradeInputField.size, 0) - }, - """ + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.StopLimit) + val errors = perp.internalState.input.errors + assertEquals(errors?.size, 2) + val error = errors?.firstOrNull() + assertEquals(error?.type, ErrorType.required) + assertEquals(error?.code, "REQUIRED_LIMIT_PRICE") + assertEquals(error?.fields?.firstOrNull(), "price.limitPrice") + assertEquals(error?.resources?.action?.stringKey, "APP.TRADE.ENTER_LIMIT_PRICE") + val error2 = errors?.get(1) + assertEquals(error2?.type, ErrorType.required) + assertEquals(error2?.code, "REQUIRED_TRIGGER_PRICE") + assertEquals(error2?.fields?.firstOrNull(), "price.triggerPrice") + assertEquals(error2?.resources?.action?.stringKey, "APP.TRADE.ENTER_TRIGGER_PRICE") + } else { + test( + { + perp.trade("1.0", TradeInputField.size, 0) + }, + """ { "input": { "current": "trade", @@ -501,14 +706,26 @@ class TradeRequiredInputTests : V4BaseTests() { ] } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } + + if (perp.staticTyping) { + perp.trade("1123", TradeInputField.limitPrice, 0) - test( - { - perp.trade("1123", TradeInputField.limitPrice, 0) - }, - """ + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.StopLimit) + val error = perp.internalState.input.errors?.firstOrNull() + assertEquals(error?.type, ErrorType.required) + assertEquals(error?.code, "REQUIRED_TRIGGER_PRICE") + assertEquals(error?.fields?.firstOrNull(), "price.triggerPrice") + assertEquals(error?.resources?.action?.stringKey, "APP.TRADE.ENTER_TRIGGER_PRICE") + } else { + test( + { + perp.trade("1123", TradeInputField.limitPrice, 0) + }, + """ { "input": { "current": "trade", @@ -531,14 +748,23 @@ class TradeRequiredInputTests : V4BaseTests() { ] } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } + + if (perp.staticTyping) { + perp.trade("1800", TradeInputField.triggerPrice, 0) - test( - { - perp.trade("1800", TradeInputField.triggerPrice, 0) - }, - """ + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.StopLimit) + val error = perp.internalState.input.errors?.firstOrNull() + assertEquals(error, null) + } else { + test( + { + perp.trade("1800", TradeInputField.triggerPrice, 0) + }, + """ { "input": { "current": "trade", @@ -548,14 +774,24 @@ class TradeRequiredInputTests : V4BaseTests() { "errors": null } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } + + if (perp.staticTyping) { + perp.trade("IOC", TradeInputField.execution, 0) - test( - { - perp.trade("IOC", TradeInputField.execution, 0) - }, - """ + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.StopLimit) + val error = perp.internalState.input.errors?.firstOrNull() + assertEquals(error?.type, ErrorType.error) + assertEquals(error?.code, "LIMIT_MUST_ABOVE_TRIGGER_PRICE") + } else { + test( + { + perp.trade("IOC", TradeInputField.execution, 0) + }, + """ { "input": { "current": "trade", @@ -570,14 +806,24 @@ class TradeRequiredInputTests : V4BaseTests() { ] } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } + + if (perp.staticTyping) { + perp.trade("1323", TradeInputField.triggerPrice, 0) - test( - { - perp.trade("1323", TradeInputField.triggerPrice, 0) - }, - """ + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.StopLimit) + val error = perp.internalState.input.errors?.firstOrNull() + assertEquals(error?.type, ErrorType.error) + assertEquals(error?.code, "TRIGGER_MUST_ABOVE_INDEX_PRICE") + } else { + test( + { + perp.trade("1323", TradeInputField.triggerPrice, 0) + }, + """ { "input": { "current": "trade", @@ -592,14 +838,24 @@ class TradeRequiredInputTests : V4BaseTests() { ] } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } + + if (perp.staticTyping) { + perp.trade("1100", TradeInputField.triggerPrice, 0) - test( - { - perp.trade("1100", TradeInputField.triggerPrice, 0) - }, - """ + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.StopLimit) + val error = perp.internalState.input.errors?.firstOrNull() + assertEquals(error?.type, ErrorType.error) + assertEquals(error?.code, "TRIGGER_MUST_ABOVE_INDEX_PRICE") + } else { + test( + { + perp.trade("1100", TradeInputField.triggerPrice, 0) + }, + """ { "input": { "current": "trade", @@ -614,16 +870,40 @@ class TradeRequiredInputTests : V4BaseTests() { ] } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } private fun testTradeInputTakeProfitLimitType() { - test( - { - perp.trade("TAKE_PROFIT", TradeInputField.type, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("TAKE_PROFIT", TradeInputField.type, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.TakeProfitLimit) + val errors = perp.internalState.input.errors + assertEquals(errors?.size, 3) + val error = errors?.firstOrNull() + assertEquals(error?.type, ErrorType.required) + assertEquals(error?.code, "REQUIRED_SIZE") + assertEquals(error?.fields?.firstOrNull(), "size.size") + assertEquals(error?.resources?.action?.stringKey, "APP.TRADE.ENTER_AMOUNT") + val error2 = errors?.get(1) + assertEquals(error2?.type, ErrorType.required) + assertEquals(error2?.code, "REQUIRED_LIMIT_PRICE") + assertEquals(error2?.fields?.firstOrNull(), "price.limitPrice") + assertEquals(error2?.resources?.action?.stringKey, "APP.TRADE.ENTER_LIMIT_PRICE") + val error3 = errors?.get(2) + assertEquals(error3?.type, ErrorType.required) + assertEquals(error3?.code, "REQUIRED_TRIGGER_PRICE") + assertEquals(error3?.fields?.firstOrNull(), "price.triggerPrice") + assertEquals(error3?.resources?.action?.stringKey, "APP.TRADE.ENTER_TRIGGER_PRICE") + } else { + test( + { + perp.trade("TAKE_PROFIT", TradeInputField.type, 0) + }, + """ { "input": { "current": "trade", @@ -670,14 +950,33 @@ class TradeRequiredInputTests : V4BaseTests() { ] } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } + + if (perp.staticTyping) { + perp.trade("1.0", TradeInputField.size, 0) - test( - { - perp.trade("1.0", TradeInputField.size, 0) - }, - """ + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.TakeProfitLimit) + val errors = perp.internalState.input.errors + assertEquals(errors?.size, 2) + val error = errors?.firstOrNull() + assertEquals(error?.type, ErrorType.required) + assertEquals(error?.code, "REQUIRED_LIMIT_PRICE") + assertEquals(error?.fields?.firstOrNull(), "price.limitPrice") + assertEquals(error?.resources?.action?.stringKey, "APP.TRADE.ENTER_LIMIT_PRICE") + val error2 = errors?.get(1) + assertEquals(error2?.type, ErrorType.required) + assertEquals(error2?.code, "REQUIRED_TRIGGER_PRICE") + assertEquals(error2?.fields?.firstOrNull(), "price.triggerPrice") + assertEquals(error2?.resources?.action?.stringKey, "APP.TRADE.ENTER_TRIGGER_PRICE") + } else { + test( + { + perp.trade("1.0", TradeInputField.size, 0) + }, + """ { "input": { "current": "trade", @@ -712,14 +1011,26 @@ class TradeRequiredInputTests : V4BaseTests() { ] } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } - test( - { - perp.trade("1123", TradeInputField.limitPrice, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("1123", TradeInputField.limitPrice, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.TakeProfitLimit) + val error = perp.internalState.input.errors?.firstOrNull() + assertEquals(error?.type, ErrorType.required) + assertEquals(error?.code, "REQUIRED_TRIGGER_PRICE") + assertEquals(error?.fields?.firstOrNull(), "price.triggerPrice") + assertEquals(error?.resources?.action?.stringKey, "APP.TRADE.ENTER_TRIGGER_PRICE") + } else { + test( + { + perp.trade("1123", TradeInputField.limitPrice, 0) + }, + """ { "input": { "current": "trade", @@ -742,14 +1053,24 @@ class TradeRequiredInputTests : V4BaseTests() { ] } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } - test( - { - perp.trade("1923", TradeInputField.triggerPrice, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("1923", TradeInputField.triggerPrice, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.TakeProfitLimit) + val error = perp.internalState.input.errors?.firstOrNull() + assertEquals(error?.type, ErrorType.error) + assertEquals(error?.code, "TRIGGER_MUST_BELOW_INDEX_PRICE") + } else { + test( + { + perp.trade("1923", TradeInputField.triggerPrice, 0) + }, + """ { "input": { "current": "trade", @@ -764,14 +1085,23 @@ class TradeRequiredInputTests : V4BaseTests() { ] } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } - test( - { - perp.trade("1290", TradeInputField.triggerPrice, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("1290", TradeInputField.triggerPrice, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.TakeProfitLimit) + val error = perp.internalState.input.errors?.firstOrNull() + assertEquals(error, null) + } else { + test( + { + perp.trade("1290", TradeInputField.triggerPrice, 0) + }, + """ { "input": { "current": "trade", @@ -781,14 +1111,24 @@ class TradeRequiredInputTests : V4BaseTests() { "errors": null } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } + + if (perp.staticTyping) { + perp.trade("IOC", TradeInputField.execution, 0) - test( - { - perp.trade("IOC", TradeInputField.execution, 0) - }, - """ + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.TakeProfitLimit) + val error = perp.internalState.input.errors?.firstOrNull() + assertEquals(error?.type, ErrorType.error) + assertEquals(error?.code, "LIMIT_MUST_ABOVE_TRIGGER_PRICE") + } else { + test( + { + perp.trade("IOC", TradeInputField.execution, 0) + }, + """ { "input": { "current": "trade", @@ -803,16 +1143,35 @@ class TradeRequiredInputTests : V4BaseTests() { ] } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } private fun testTradeInputTrailingStopType() { - test( - { - perp.trade("TRAILING_STOP", TradeInputField.type, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("TRAILING_STOP", TradeInputField.type, 0) + + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.TrailingStop) + val errors = perp.internalState.input.errors + assertEquals(errors?.size, 2) + val error = errors?.firstOrNull() + assertEquals(error?.type, ErrorType.required) + assertEquals(error?.code, "REQUIRED_SIZE") + assertEquals(error?.fields?.firstOrNull(), "size.size") + assertEquals(error?.resources?.action?.stringKey, "APP.TRADE.ENTER_AMOUNT") + val error2 = errors?.get(1) + assertEquals(error2?.type, ErrorType.required) + assertEquals(error2?.code, "REQUIRED_TRAILING_PERCENT") + assertEquals(error2?.fields?.firstOrNull(), "price.trailingPercent") + assertEquals(error2?.resources?.action?.stringKey, "APP.TRADE.ENTER_TRAILING_PERCENT") + } else { + test( + { + perp.trade("TRAILING_STOP", TradeInputField.type, 0) + }, + """ { "input": { "current": "trade", @@ -847,14 +1206,26 @@ class TradeRequiredInputTests : V4BaseTests() { ] } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } + + if (perp.staticTyping) { + perp.trade("1.0", TradeInputField.size, 0) - test( - { - perp.trade("1.0", TradeInputField.size, 0) - }, - """ + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.TrailingStop) + val error = perp.internalState.input.errors?.firstOrNull() + assertEquals(error?.type, ErrorType.required) + assertEquals(error?.code, "REQUIRED_TRAILING_PERCENT") + assertEquals(error?.fields?.firstOrNull(), "price.trailingPercent") + assertEquals(error?.resources?.action?.stringKey, "APP.TRADE.ENTER_TRAILING_PERCENT") + } else { + test( + { + perp.trade("1.0", TradeInputField.size, 0) + }, + """ { "input": { "current": "trade", @@ -877,14 +1248,23 @@ class TradeRequiredInputTests : V4BaseTests() { ] } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } + + if (perp.staticTyping) { + perp.trade("0.05", TradeInputField.trailingPercent, 0) - test( - { - perp.trade("0.05", TradeInputField.trailingPercent, 0) - }, - """ + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.TrailingStop) + val error = perp.internalState.input.errors?.firstOrNull() + assertEquals(error, null) + } else { + test( + { + perp.trade("0.05", TradeInputField.trailingPercent, 0) + }, + """ { "input": { "current": "trade", @@ -894,7 +1274,8 @@ class TradeRequiredInputTests : V4BaseTests() { "errors": null } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } } diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/validation/ValidationsTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/validation/ValidationsTests.kt index 1fdde82ed..694a94924 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/validation/ValidationsTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/validation/ValidationsTests.kt @@ -9,6 +9,8 @@ open class ValidationsTests : V4BaseTests() { override fun setup() { super.setup() + perp.internalState.wallet.walletAddress = "0x1234567890" + test({ loadValidationsMarkets() }, null) diff --git a/v4_abacus.podspec b/v4_abacus.podspec index 31c1ef50a..f440015cd 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.9.6' + spec.version = '1.9.7' spec.homepage = 'https://github.com/dydxprotocol/v4-abacus' spec.source = { :http=> ''} spec.authors = ''