From 1123912f19c66664b537173c4689ec005a37cab4 Mon Sep 17 00:00:00 2001 From: tyleroooo Date: Thu, 22 Aug 2024 15:26:35 -0400 Subject: [PATCH] feat: vaults helper functions (#578) Co-authored-by: mobile-build-bot-git --- build.gradle.kts | 2 +- .../functional/vault/Vault.kt | 231 ++++++++++++++++++ .../functional/vault/VaultAccount.kt | 92 +++++++ .../functional/vault/VaultIndexerModels.kt | 41 ++++ .../functional/vault/VaultAccountTests.kt | 67 +++++ .../functional/vault/VaultTests.kt | 205 ++++++++++++++++ v4_abacus.podspec | 2 +- 7 files changed, 638 insertions(+), 2 deletions(-) create mode 100644 src/commonMain/kotlin/exchange.dydx.abacus/functional/vault/Vault.kt create mode 100644 src/commonMain/kotlin/exchange.dydx.abacus/functional/vault/VaultAccount.kt create mode 100644 src/commonMain/kotlin/exchange.dydx.abacus/functional/vault/VaultIndexerModels.kt create mode 100644 src/commonTest/kotlin/exchange.dydx.abacus/functional/vault/VaultAccountTests.kt create mode 100644 src/commonTest/kotlin/exchange.dydx.abacus/functional/vault/VaultTests.kt diff --git a/build.gradle.kts b/build.gradle.kts index 8427d025a..988f17603 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -52,7 +52,7 @@ allprojects { } group = "exchange.dydx.abacus" -version = "1.8.98" +version = "1.8.99" repositories { google() diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/functional/vault/Vault.kt b/src/commonMain/kotlin/exchange.dydx.abacus/functional/vault/Vault.kt new file mode 100644 index 000000000..7fcc98743 --- /dev/null +++ b/src/commonMain/kotlin/exchange.dydx.abacus/functional/vault/Vault.kt @@ -0,0 +1,231 @@ +package exchange.dydx.abacus.functional.vault +import exchange.dydx.abacus.calculator.CalculationPeriod +import exchange.dydx.abacus.calculator.v2.SubaccountCalculatorV2 +import exchange.dydx.abacus.output.PerpetualMarket +import exchange.dydx.abacus.processor.wallet.account.AssetPositionProcessor +import exchange.dydx.abacus.processor.wallet.account.PerpetualPositionProcessor +import exchange.dydx.abacus.protocols.asTypedObject +import exchange.dydx.abacus.state.internalstate.InternalMarketState +import exchange.dydx.abacus.state.internalstate.InternalMarketSummaryState +import exchange.dydx.abacus.state.internalstate.InternalSubaccountCalculated +import exchange.dydx.abacus.state.internalstate.InternalSubaccountState +import exchange.dydx.abacus.utils.IMap +import exchange.dydx.abacus.utils.Parser +import kotlinx.datetime.Clock +import kotlinx.serialization.Serializable +import kotlin.js.JsExport +import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.milliseconds + +@JsExport +@Serializable +data class VaultDetails( + val totalValue: Double? = null, + val thirtyDayReturnPercent: Double? = null, + val history: List? = null +) + +@JsExport +@Serializable +data class VaultPositions( + val positions: List? = null, +) + +@JsExport +@Serializable +data class VaultHistoryEntry( + val date: Double? = null, + val equity: Double? = null, + val totalPnl: Double? = null +) + +@JsExport +@Serializable +data class VaultPosition( + val marketId: String? = null, + val marginUsdc: Double? = null, + val equityUsdc: Double? = null, + val currentLeverageMultiple: Double? = null, + val currentPosition: CurrentPosition? = null, + val thirtyDayPnl: ThirtyDayPnl? = null +) + +@JsExport +@Serializable +data class CurrentPosition( + val asset: Double? = null, + val usdc: Double? = null +) + +@JsExport +@Serializable +data class ThirtyDayPnl( + val percent: Double? = null, + val absolute: Double? = null, + val sparklinePoints: List? = null +) + +@JsExport +object VaultCalculator { + private val parser = Parser() + private val perpetualPositionProcessor = PerpetualPositionProcessor(parser, null) + private val assetPositionProcessor = AssetPositionProcessor(parser) + private val subaccountCalculator = SubaccountCalculatorV2(parser) + + fun getVaultHistoricalPnlResponse(apiResponse: String): IndexerVaultHistoricalPnlResponse? { + return parser.asTypedObject(apiResponse) + } + + fun getSubvaultHistoricalPnlResponse(apiResponse: String): IndexerSubvaultHistoricalPnlResponse? { + return parser.asTypedObject(apiResponse) + } + + fun getVaultPositionsResponse(apiResponse: String): IndexerVaultPositionResponse? { + return parser.asTypedObject(apiResponse) + } + + fun calculateVaultSummary(historical: IndexerVaultHistoricalPnlResponse?): VaultDetails? { + if (historical?.vaultOfVaultsPnl.isNullOrEmpty()) { + return null + } + + val vaultOfVaultsPnl = historical!!.vaultOfVaultsPnl!!.sortedByDescending { parser.asDouble(it.createdAt) } + + val history = vaultOfVaultsPnl.mapNotNull { entry -> + parser.asDouble(entry.createdAt)?.let { createdAt -> + VaultHistoryEntry( + date = createdAt, + equity = parser.asDouble(entry.equity) ?: 0.0, + totalPnl = parser.asDouble(entry.totalPnl) ?: 0.0, + ) + } + } + + val latestEntry = history.first() + val latestTime = latestEntry.date ?: Clock.System.now().toEpochMilliseconds().toDouble() + val thirtyDaysAgoTime = latestTime - 30.days.inWholeMilliseconds + + val thirtyDaysAgoEntry = history.find { + (it.date ?: Double.MAX_VALUE) <= thirtyDaysAgoTime + } ?: history.last() + + val totalValue = latestEntry.equity ?: 0.0 + + val latestTotalPnl = latestEntry.totalPnl ?: 0.0 + val thirtyDaysAgoTotalPnl = thirtyDaysAgoEntry.totalPnl ?: 0.0 + + val pnlDifference = latestTotalPnl - thirtyDaysAgoTotalPnl + val thirtyDaysAgoEquity = thirtyDaysAgoEntry.equity ?: 0.0 + val thirtyDayReturnPercent = if (thirtyDaysAgoEquity != 0.0) { + (pnlDifference / thirtyDaysAgoEquity) + } else { + 0.0 + } + + return VaultDetails( + totalValue = totalValue, + thirtyDayReturnPercent = thirtyDayReturnPercent, + history = history, + ) + } + + fun calculateVaultPositions( + positions: IndexerVaultPositionResponse?, + histories: IndexerSubvaultHistoricalPnlResponse?, + markets: IMap? + ): VaultPositions? { + if (positions?.positions == null) { + return null + } + + val historiesMap = histories?.vaultsPnl?.associateBy { it.marketId } + + return VaultPositions(positions = positions.positions.mapNotNull { calculateVaultPosition(it, historiesMap?.get(it.market), markets?.get(it.market)) }) + } + + fun calculateVaultPosition(position: IndexerVaultPosition, history: IndexerVaultHistoricalPnl?, perpetualMarket: PerpetualMarket?): VaultPosition? { + if (position.market == null) { + return null + } + val perpetualPosition = perpetualPositionProcessor.process(null, position.perpetualPosition) + val assetPosition = assetPositionProcessor.process(position.assetPosition) + + val assetPositionsMap = assetPosition?.let { mapOf((it.assetId ?: "") to it) } + val subaccount = subaccountCalculator.calculate( + subaccount = InternalSubaccountState( + equity = parser.asDouble(position.equity) ?: 0.0, + assetPositions = assetPositionsMap, + openPositions = perpetualPosition?.let { mapOf((it.market ?: "") to it) }, + subaccountNumber = 0, + calculated = mutableMapOf( + CalculationPeriod.current to + InternalSubaccountCalculated(quoteBalance = subaccountCalculator.calculateQuoteBalance(assetPositionsMap)), + ), + + ), + marketsSummary = InternalMarketSummaryState(markets = mutableMapOf(position.market to InternalMarketState(perpetualMarket = perpetualMarket))), + periods = setOf(CalculationPeriod.current), + price = null, + configs = null, + ) + val calculated = subaccount?.calculated?.get(CalculationPeriod.current) + val perpCalculated = perpetualPosition?.calculated?.get(CalculationPeriod.current) + return VaultPosition( + marketId = position.market, + marginUsdc = calculated?.equity, + currentLeverageMultiple = perpCalculated?.leverage, + currentPosition = CurrentPosition( + asset = perpCalculated?.size, + usdc = perpCalculated?.notionalTotal, + ), + thirtyDayPnl = calculateThirtyDayPnl(history), + ) + } + + private fun calculateThirtyDayPnl(vaultHistoricalPnl: IndexerVaultHistoricalPnl?): ThirtyDayPnl? { + val historicalPnl = vaultHistoricalPnl?.historicalPnl ?: return null + + if (historicalPnl.isEmpty()) { + return null + } + + val sortedPnl = historicalPnl.sortedByDescending { parser.asLong(it.createdAt) } + val latestEntry = sortedPnl.first() + val latestTime = parser.asLong(latestEntry.createdAt) ?: Clock.System.now().toEpochMilliseconds() + val thirtyDaysAgoTime = latestTime - 30.days.inWholeMilliseconds + + val thirtyDaysAgoEntry = sortedPnl.find { + (parser.asLong(it.createdAt) ?: Long.MAX_VALUE) <= thirtyDaysAgoTime + } ?: sortedPnl.last() + + val latestTotalPnl = parser.asDouble(latestEntry.totalPnl) ?: 0.0 + val thirtyDaysAgoTotalPnl = parser.asDouble(thirtyDaysAgoEntry.totalPnl) ?: 0.0 + val absolutePnl = latestTotalPnl - thirtyDaysAgoTotalPnl + + val thirtyDaysAgoEquity = parser.asDouble(thirtyDaysAgoEntry.equity) ?: 0.0 + val percentPnl = if (thirtyDaysAgoEquity != 0.0) { + (absolutePnl / thirtyDaysAgoEquity) + } else { + 0.0 + } + + val sparklinePoints = sortedPnl + .takeWhile { (parser.asLong(it.createdAt) ?: Long.MAX_VALUE) >= thirtyDaysAgoTime } + .groupBy { entry -> + val timestamp = parser.asLong(entry.createdAt) ?: 0L + timestamp.milliseconds.inWholeDays + } + .mapValues { (_, entries) -> + parser.asDouble(entries.first().totalPnl) ?: 0.0 + } + .toList() + .sortedBy { (day, _) -> day } + .map { (_, value) -> value } + + return ThirtyDayPnl( + percent = percentPnl, + absolute = absolutePnl, + sparklinePoints = sparklinePoints, + ) + } +} diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/functional/vault/VaultAccount.kt b/src/commonMain/kotlin/exchange.dydx.abacus/functional/vault/VaultAccount.kt new file mode 100644 index 000000000..3aebee8f6 --- /dev/null +++ b/src/commonMain/kotlin/exchange.dydx.abacus/functional/vault/VaultAccount.kt @@ -0,0 +1,92 @@ +package exchange.dydx.abacus.functional.vault + +import exchange.dydx.abacus.protocols.asTypedObject +import exchange.dydx.abacus.utils.Parser +import indexer.codegen.IndexerTransferBetweenResponse +import indexer.codegen.IndexerTransferType.DEPOSIT +import indexer.codegen.IndexerTransferType.TRANSFERIN +import indexer.codegen.IndexerTransferType.TRANSFEROUT +import indexer.codegen.IndexerTransferType.WITHDRAWAL +import kotlinx.serialization.Serializable +import kotlin.js.JsExport + +@JsExport +@Serializable +data class AccountVaultResponse( + val address: String? = null, + val shares: Double? = null, + @Suppress("ConstructorParameterNaming") + val locked_shares: Double? = null, + val equity: Double? = null, + @Suppress("ConstructorParameterNaming") + val withdrawable_amount: Double? = null, +) + +@JsExport +@Serializable +data class VaultAccount( + val balanceUsdc: Double?, + val withdrawableUsdc: Double?, + val allTimeReturnUsdc: Double?, + val vaultTransfers: List?, + val totalVaultTransfersCount: Int?, +) + +@JsExport +@Serializable +data class VaultTransfer( + val timestampMs: Double?, + val amountUsdc: Double?, + val type: VaultTransferType?, + val id: String?, +) + +@JsExport +@Serializable +enum class VaultTransferType { + WITHDRAWAL, + DEPOSIT +} + +@JsExport +object VaultAccountCalculator { + private val parser = Parser() + + fun getAccountVaultResponse(apiResponse: String): AccountVaultResponse? { + return parser.asTypedObject(apiResponse) + } + + fun getTransfersBetweenResponse(apiResponse: String): IndexerTransferBetweenResponse? { + return parser.asTypedObject(apiResponse) + } + + fun calculateUserVaultInfo( + vaultInfo: AccountVaultResponse, + vaultTransfers: IndexerTransferBetweenResponse + ): VaultAccount { + val presentValue = vaultInfo.equity + val netTransfers = parser.asDouble(vaultTransfers.totalNetTransfers) + val withdrawable = vaultInfo.withdrawable_amount + val allTimeReturn = + if (presentValue != null && netTransfers != null) (presentValue - netTransfers) else null + + return VaultAccount( + balanceUsdc = presentValue, + withdrawableUsdc = withdrawable, + allTimeReturnUsdc = allTimeReturn, + totalVaultTransfersCount = vaultTransfers.totalResults, + vaultTransfers = vaultTransfers.transfersSubset?.map { el -> + VaultTransfer( + timestampMs = parser.asDouble(el.createdAt), + amountUsdc = parser.asDouble(el.size), + type = when (el.type) { + TRANSFEROUT -> VaultTransferType.DEPOSIT + TRANSFERIN -> VaultTransferType.WITHDRAWAL + DEPOSIT, WITHDRAWAL, null -> null + }, + id = el.id, + ) + }, + ) + } +} diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/functional/vault/VaultIndexerModels.kt b/src/commonMain/kotlin/exchange.dydx.abacus/functional/vault/VaultIndexerModels.kt new file mode 100644 index 000000000..99b6578c3 --- /dev/null +++ b/src/commonMain/kotlin/exchange.dydx.abacus/functional/vault/VaultIndexerModels.kt @@ -0,0 +1,41 @@ +package exchange.dydx.abacus.functional.vault + +import indexer.codegen.IndexerAssetPositionResponseObject +import indexer.codegen.IndexerPerpetualPositionResponseObject +import indexer.codegen.IndexerPnlTicksResponseObject +import kotlinx.serialization.Serializable +import kotlin.js.JsExport + +@JsExport +@Serializable +data class IndexerVaultHistoricalPnlResponse( + val vaultOfVaultsPnl: List? = null +) + +@JsExport +@Serializable +data class IndexerVaultHistoricalPnl( + val marketId: String? = null, + val historicalPnl: List? = null +) + +@JsExport +@Serializable +data class IndexerSubvaultHistoricalPnlResponse( + val vaultsPnl: List? = null +) + +@JsExport +@Serializable +data class IndexerVaultPosition( + val market: String? = null, + val assetPosition: IndexerAssetPositionResponseObject? = null, + val perpetualPosition: IndexerPerpetualPositionResponseObject? = null, + val equity: String? = null +) + +@JsExport +@Serializable +data class IndexerVaultPositionResponse( + val positions: List? = null +) diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/functional/vault/VaultAccountTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/functional/vault/VaultAccountTests.kt new file mode 100644 index 000000000..a23eccb47 --- /dev/null +++ b/src/commonTest/kotlin/exchange.dydx.abacus/functional/vault/VaultAccountTests.kt @@ -0,0 +1,67 @@ +package exchange.dydx.abacus.functional.vault + +import exchange.dydx.abacus.functional.vault.VaultAccountCalculator.calculateUserVaultInfo +import indexer.codegen.IndexerTransferBetweenResponse +import indexer.codegen.IndexerTransferResponseObject +import indexer.codegen.IndexerTransferType +import kotlin.test.Test +import kotlin.test.assertEquals + +// transformation is pretty simple, so we just test basic input+output +class VaultAccountTests { + + @Test + fun calculateUserVaultInfo_basic() { + val vaultInfo = AccountVaultResponse( + address = "0x123", + shares = 100.0, + locked_shares = 50.0, + equity = 10000.0, + withdrawable_amount = 5000.0, + ) + + val vaultTransfers = IndexerTransferBetweenResponse( + totalResults = 2, + totalNetTransfers = "4000.0", + transfersSubset = arrayOf( + IndexerTransferResponseObject( + id = "1", + createdAt = "1659465600000", + size = "6000.0", + type = IndexerTransferType.TRANSFEROUT, + ), + IndexerTransferResponseObject( + id = "2", + createdAt = "1659552000000", + size = "2000.0", + type = IndexerTransferType.TRANSFERIN, + ), + ), + ) + + val vaultAccount = calculateUserVaultInfo(vaultInfo, vaultTransfers) + + val expectedVaultAccount = VaultAccount( + balanceUsdc = 10000.0, + withdrawableUsdc = 5000.0, + allTimeReturnUsdc = 6000.0, + totalVaultTransfersCount = 2, + vaultTransfers = listOf( + VaultTransfer( + timestampMs = 1659465600000.0, + amountUsdc = 6000.0, + type = VaultTransferType.DEPOSIT, + id = "1", + ), + VaultTransfer( + timestampMs = 1659552000000.0, + amountUsdc = 2000.0, + type = VaultTransferType.WITHDRAWAL, + id = "2", + ), + ), + ) + + assertEquals(expectedVaultAccount, vaultAccount) + } +} diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/functional/vault/VaultTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/functional/vault/VaultTests.kt new file mode 100644 index 000000000..d9f7ecbe1 --- /dev/null +++ b/src/commonTest/kotlin/exchange.dydx.abacus/functional/vault/VaultTests.kt @@ -0,0 +1,205 @@ +package exchange.dydx.abacus.functional.vault + +import exchange.dydx.abacus.functional.vault.VaultCalculator.calculateVaultPosition +import exchange.dydx.abacus.functional.vault.VaultCalculator.calculateVaultSummary +import exchange.dydx.abacus.output.PerpetualMarket +import exchange.dydx.abacus.utils.NUM_PARENT_SUBACCOUNTS +import indexer.codegen.IndexerAssetPositionResponseObject +import indexer.codegen.IndexerPerpetualPositionResponseObject +import indexer.codegen.IndexerPerpetualPositionStatus +import indexer.codegen.IndexerPnlTicksResponseObject +import indexer.codegen.IndexerPositionSide +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.time.Duration.Companion.days + +class VaultTests { + + @Test + fun calculateVaultSummary_basic() { + val historicalPnl = IndexerVaultHistoricalPnlResponse( + vaultOfVaultsPnl = listOf( + IndexerPnlTicksResponseObject( + equity = "10000.0", + totalPnl = "1000.0", + netTransfers = "0.0", + createdAt = "1659465600000", + ), + IndexerPnlTicksResponseObject( + equity = "5000.0", + totalPnl = "500", + netTransfers = "0.0", + createdAt = "1659379200000", + ), + ), + ) + + val vaultDetails = calculateVaultSummary(historicalPnl) + + val expectedVaultDetails = VaultDetails( + totalValue = 10000.0, + thirtyDayReturnPercent = 0.1, + history = listOf( + VaultHistoryEntry( + date = 1659465600000.0, + equity = 10000.0, + totalPnl = 1000.0, + ), + VaultHistoryEntry( + date = 1659379200000.0, + equity = 5000.0, + totalPnl = 500.0, + ), + ), + ) + + assertEquals(expectedVaultDetails, vaultDetails) + } + + @Test + fun shouldReturnNullForNullOrEmptyHistoricalPnl() { + val nullHistoricalPnl = IndexerVaultHistoricalPnlResponse(vaultOfVaultsPnl = null) + val emptyHistoricalPnl = IndexerVaultHistoricalPnlResponse(vaultOfVaultsPnl = emptyList()) + + val nullVaultDetails = calculateVaultSummary(nullHistoricalPnl) + val emptyVaultDetails = calculateVaultSummary(emptyHistoricalPnl) + + assertEquals(null, nullVaultDetails) + assertEquals(null, emptyVaultDetails) + } + + @Test + fun shouldCalculate30DayReturnCorrectly() { + val latestTimestamp = 1659465600000L + val thirtyOneDaysAgoTimestamp = latestTimestamp - 31.days.inWholeMilliseconds + val thirtyDaysAgoTimestamp = latestTimestamp - 30.days.inWholeMilliseconds + val twentyNineDaysAgoTimestamp = latestTimestamp - 29.days.inWholeMilliseconds + + val historicalPnl = IndexerVaultHistoricalPnlResponse( + vaultOfVaultsPnl = listOf( + IndexerPnlTicksResponseObject( + equity = "10000.0", + totalPnl = "1000.0", + netTransfers = "0.0", + createdAt = latestTimestamp.toString(), + ), + IndexerPnlTicksResponseObject( + equity = "9700.0", + totalPnl = "700.0", + netTransfers = "0.0", + createdAt = twentyNineDaysAgoTimestamp.toString(), + ), + IndexerPnlTicksResponseObject( + equity = "9500.0", + totalPnl = "500.0", + netTransfers = "0.0", + createdAt = thirtyDaysAgoTimestamp.toString(), + ), + IndexerPnlTicksResponseObject( + equity = "9300.0", + totalPnl = "300.0", + netTransfers = "0.0", + createdAt = thirtyOneDaysAgoTimestamp.toString(), + ), + IndexerPnlTicksResponseObject( + equity = "9000.0", + totalPnl = "0.0", + netTransfers = "0.0", + createdAt = (thirtyDaysAgoTimestamp - 7.days.inWholeMilliseconds).toString(), + ), + ), + ) + + val vaultDetails = calculateVaultSummary(historicalPnl) + + assertNotNull(vaultDetails) + assertEquals(0.05263157894736842, vaultDetails.thirtyDayReturnPercent) + } + + @Test + fun shouldCalculateVaultPositionCorrectly() { + val position = IndexerVaultPosition( + market = "BTC-USD", + assetPosition = IndexerAssetPositionResponseObject( + symbol = "USDC", + side = IndexerPositionSide.SHORT, + size = "40000.0", + assetId = "USDC", + subaccountNumber = NUM_PARENT_SUBACCOUNTS, + ), + perpetualPosition = IndexerPerpetualPositionResponseObject( + market = "BTC-USD", + status = IndexerPerpetualPositionStatus.OPEN, + side = IndexerPositionSide.LONG, + size = "1.0", + maxSize = null, + entryPrice = "50000.0", + realizedPnl = null, + createdAt = "2023-08-01T00:00:00Z", + createdAtHeight = "1000", + sumOpen = null, + sumClose = null, + netFunding = null, + unrealizedPnl = "5000.0", + closedAt = null, + exitPrice = null, + subaccountNumber = NUM_PARENT_SUBACCOUNTS, + ), + equity = "15000.0", + ) + + val history = IndexerVaultHistoricalPnl( + marketId = "BTC-USD", + historicalPnl = listOf( + IndexerPnlTicksResponseObject( + id = "1", + equity = "10500.0", + totalPnl = "500.0", + netTransfers = "0.0", + createdAt = "1659465600000", + ), + IndexerPnlTicksResponseObject( + id = "2", + equity = "10000.0", + totalPnl = "0.0", + netTransfers = "0.0", + createdAt = "1659379200000", + ), + ), + ) + + val market = PerpetualMarket( + id = "BTC-USD", + assetId = "BTC", + market = "BTC-USD", + displayId = null, + oraclePrice = 55000.0, + marketCaps = null, + priceChange24H = null, + priceChange24HPercent = null, + status = null, + configs = null, + perpetual = null, + ) + + val vaultPosition = calculateVaultPosition(position, history, market) + + val expectedVaultPosition = VaultPosition( + marketId = "BTC-USD", + marginUsdc = 15000.0, + currentLeverageMultiple = 55.0 / 15.0, + currentPosition = CurrentPosition( + asset = 1.0, + usdc = 55000.0, + ), + thirtyDayPnl = ThirtyDayPnl( + percent = 0.05, + absolute = 500.0, + sparklinePoints = listOf(0.0, 500.0), + ), + ) + + assertEquals(expectedVaultPosition, vaultPosition) + } +} diff --git a/v4_abacus.podspec b/v4_abacus.podspec index edfd9d764..9e8038548 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.8.98' + spec.version = '1.8.99' spec.homepage = 'https://github.com/dydxprotocol/v4-abacus' spec.source = { :http=> ''} spec.authors = ''